Skip to content

Commit

Permalink
Merge pull request #7445 from uktrade/refactor/dates
Browse files Browse the repository at this point in the history
Refactor and simplify date utility functions
  • Loading branch information
paulgain authored Jan 7, 2025
2 parents 604b22c + 6fe0047 commit 02a2405
Show file tree
Hide file tree
Showing 12 changed files with 249 additions and 72 deletions.
4 changes: 2 additions & 2 deletions src/apps/interactions/apps/details-form/client/tasks.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import {
DATE_FORMAT_YEAR,
} from '../../../../../client/utils/date-utils'

const { transformValueForAPI } = require('../../../../../client/utils/date')
const { formatDateWithYearMonth } = require('../../../../../client/utils/date')

const FIELDS_TO_OMIT = [
'currently_exporting',
Expand Down Expand Up @@ -276,7 +276,7 @@ export function saveInteraction({ values, companyIds, referralId }) {
dit_participants: values.dit_participants.map((a) => ({
adviser: a.value,
})),
date: transformValueForAPI(values.date),
date: formatDateWithYearMonth(values.date),
policy_areas: transformArrayOfOptionsToValues(values.policy_areas),
communication_channel: transformOptionToValue(values.communication_channel),
event: transformOptionToValue(values.event),
Expand Down
12 changes: 4 additions & 8 deletions src/client/components/Form/elements/FieldDate/index.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react'
import PropTypes from 'prop-types'
import { isValid } from 'date-fns'
import { castArray, snakeCase } from 'lodash'
import styled from 'styled-components'
import ErrorText from '@govuk-react/error-text'
Expand All @@ -16,10 +17,7 @@ import FieldWrapper from '../FieldWrapper'
import useField from '../../hooks/useField'
import { useFormContext } from '../../hooks'

const {
isNormalisedDateValid,
isShortDateValid,
} = require('../../../../utils/date')
const { parseDateWithYearMonth } = require('../../../../utils/date')

const DAY = 'day'
const MONTH = 'month'
Expand Down Expand Up @@ -56,10 +54,8 @@ const StyledList = styled.div`
const getValidator =
(required, invalid, format) =>
({ day, month, year }) => {
const isDateValid = isValid(parseDateWithYearMonth(year, month, day))
const isLong = format === FORMAT_LONG
const isValid = isLong
? isNormalisedDateValid(year, month, day)
: isShortDateValid(year, month)

const isDateEmpty = isLong ? !day && !month && !year : !month && !year

Expand All @@ -71,7 +67,7 @@ const getValidator =
return required
}

if (!isValid && !isDateEmpty) {
if (!isDateValid && !isDateEmpty) {
return invalid || 'Enter a valid date'
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { transformDateStringToDateObject } from '../../../transformers'
import { transformValueForAPI } from '../../../utils/date'
import { formatDateWithYearMonth } from '../../../utils/date'
import {
transformBoolToRadioOption,
transformRadioOptionToBool,
Expand All @@ -16,7 +16,7 @@ export const transformFormValuesForAPI = ({
}) => ({
subject,
detail,
target_date: transformValueForAPI(target_date),
target_date: formatDateWithYearMonth(target_date),
company,
has_blocker: transformRadioOptionToBool(has_blocker),
blocker_description,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { isBoolean, isNumber } from 'lodash'
import { isValid } from 'date-fns'

import { transformFieldName } from '../../../components/AuditHistory/transformers'
import { CONTACT_FIELD_NAME_TO_LABEL_MAP } from './constants'
import { isUnparsedDateValid } from '../../../utils/date'
import {
formatDate,
DATE_FORMAT_MEDIUM_WITH_TIME,
Expand All @@ -29,6 +29,6 @@ export const getValue = (value, field) =>
: NO
: isNumber(value)
? value.toString()
: isUnparsedDateValid(value)
: isValid(value)
? formatDate(value, DATE_FORMAT_MEDIUM_WITH_TIME)
: value || NOT_SET
4 changes: 2 additions & 2 deletions src/client/modules/Investments/Opportunities/tasks.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import urls from '../../../../lib/urls'
import { idNamesToValueLabels } from '../../../utils'
import { apiProxyAxios } from '../../../components/Task/utils'
import { transformValueForAPI } from '../../../utils/date'
import { formatDateWithYearMonth } from '../../../utils/date'
import { getMetadataOptions } from '../../../metadata'

import { transformInvestmentOpportunityDetails } from './transformers'
Expand Down Expand Up @@ -47,7 +47,7 @@ export function saveOpportunityDetails({ values, opportunityId }) {
: undefined,
required_checks_conducted_by: values.requiredChecksConductedBy?.value,
required_checks_conducted_on: values.requiredChecksConductedOn
? transformValueForAPI(values.requiredChecksConductedOn)
? formatDateWithYearMonth(values.requiredChecksConductedOn)
: undefined,
lead_dit_relationship_manager: values.leadRelationshipManager?.value,
other_dit_contacts: values.otherDitContacts?.map(({ value }) => value),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { isBoolean, isNumber } from 'lodash'
import { isValid } from 'date-fns'

import { PROJECT_FIELD_NAME_TO_LABEL_MAP } from './constants'
import { isUnparsedDateValid } from '../../../../utils/date'
import { formatDate, DATE_FORMAT_MEDIUM } from '../../../../utils/date-utils'
import { currencyGBP } from '../../../../utils/number-utils'
import { NOT_SET, NO, YES } from '../../../../components/AuditHistory/constants'
Expand Down Expand Up @@ -36,6 +36,6 @@ export const getValue = (value, field) =>
? value.toString()
: Array.isArray(value)
? value.join(', ')
: isUnparsedDateValid(value)
: isValid(value)
? formatDate(value, DATE_FORMAT_MEDIUM)
: value || NOT_SET
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react'
import { Link } from 'govuk-react'
import styled from 'styled-components'

import { transformValueForAPI } from '../../../../utils/date'
import { formatDateWithYearMonth } from '../../../../utils/date'
import { VIRUS_SCAN_STATUSES } from '../constants'

import urls from '../../../../../lib/urls'
Expand Down Expand Up @@ -32,7 +32,7 @@ export const transformPropositionForAPI = ({ projectId, values }) => {
deadline_day: proposition_deadline.day,
deadline_month: proposition_deadline.month,
deadline_year: proposition_deadline.year,
deadline: transformValueForAPI(proposition_deadline),
deadline: formatDateWithYearMonth(proposition_deadline),
}
}

Expand Down
63 changes: 63 additions & 0 deletions src/client/modules/Tasks/TaskForm/__test__/validators.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { addDays, subDays, format } from 'date-fns'

import { validateIfDateInFuture } from '../validators'

describe('validateIfDateInFuture', () => {
it('should return null for a date in the future (with day provided)', () => {
const futureDate = addDays(new Date(), 10)
const year = format(futureDate, 'yyyy')
const month = format(futureDate, 'MM')
const day = format(futureDate, 'dd')

const result = validateIfDateInFuture({ year, month, day })
expect(result).to.be.null
})

it('should return null for a date in the future (year and month only)', () => {
const futureDate = addDays(new Date(), 40) // Ensures crossing month boundary
const year = format(futureDate, 'yyyy')
const month = format(futureDate, 'MM')

const result = validateIfDateInFuture({ year, month })
expect(result).to.be.null
})

it('should return error message for a past date (with day provided)', () => {
const pastDate = subDays(new Date(), 10)
const year = format(pastDate, 'yyyy')
const month = format(pastDate, 'MM')
const day = format(pastDate, 'dd')

const result = validateIfDateInFuture({ year, month, day })
expect(result).to.equal('Enter a date in the future')
})

it('should return error message for a past date (year and month only)', () => {
const pastDate = subDays(new Date(), 40)
const year = format(pastDate, 'yyyy')
const month = format(pastDate, 'MM')

const result = validateIfDateInFuture({ year, month })
expect(result).to.equal('Enter a date in the future')
})

it('should handle edge cases correctly (today is not in the future)', () => {
const today = new Date()
const year = format(today, 'yyyy')
const month = format(today, 'MM')
const day = format(today, 'dd')

const result = validateIfDateInFuture({ year, month, day })
expect(result).to.equal('Enter a date in the future')
})

it('should handle invalid inputs gracefully', () => {
const invalidMessage = 'Enter a date in the future'

expect(validateIfDateInFuture({})).to.equal(invalidMessage)
expect(validateIfDateInFuture({ year: '2025' })).to.equal(invalidMessage)
expect(validateIfDateInFuture({ year: '2025', month: '13' })).to.equal(
invalidMessage
)
})
})
4 changes: 2 additions & 2 deletions src/client/modules/Tasks/TaskForm/transformers.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
transformIdNameToValueLabel,
} from '../../../transformers'
import {
transformValueForAPI,
formatDateWithYearMonth,
convertDateToFieldDateObject,
} from '../../../utils/date'
import { formatDate, DATE_FORMAT_ISO } from '../../../utils/date-utils'
Expand Down Expand Up @@ -61,7 +61,7 @@ export const getDueDate = (dueDate, customDate) => {
const today = new Date()

const handlers = {
custom: () => transformValueForAPI(customDate),
custom: () => formatDateWithYearMonth(customDate),
month: () => formatDate(addMonths(today, 1), DATE_FORMAT_ISO),
week: () => formatDate(addDays(today, 7), DATE_FORMAT_ISO),
}
Expand Down
8 changes: 5 additions & 3 deletions src/client/modules/Tasks/TaskForm/validators.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { isFuture } from 'date-fns'

import { transformValueForAPI } from '../../../utils/date'
import { parseDateWithYearMonth } from '../../../utils/date'

export const validateIfDateInFuture = (values) =>
isFuture(transformValueForAPI(values)) ? null : 'Enter a date in the future'
export const validateIfDateInFuture = ({ year, month, day }) =>
isFuture(parseDateWithYearMonth(year, month, day))
? null
: 'Enter a date in the future'

export const validateDaysRange = (value) =>
!value || value < 1 || value > 365 ? 'Enter a number between 1 and 365' : null
112 changes: 111 additions & 1 deletion src/client/utils/__test__/date.test.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { subMonths, subYears, addDays } from 'date-fns'
import { subMonths, subYears, addDays, isValid, format } from 'date-fns'

import {
areDatesEqual,
getStartOfMonth,
getRandomDateInRange,
parseDateWithYearMonth,
formatDateWithYearMonth,
isWithinLastTwelveMonths,
convertDateToFieldDateObject,
convertDateToFieldShortDateObject,
Expand Down Expand Up @@ -129,3 +131,111 @@ describe('getRandomDateInRange', () => {
)
})
})

describe('parseDateWithYearMonth', () => {
it('should parse a valid full date (yyyy-MM-dd)', () => {
const date = parseDateWithYearMonth('2024', '12', '31')
expect(isValid(date)).to.be.true
expect(format(date, 'yyyy-MM-dd')).to.equal('2024-12-31')
})

it('should parse a valid year and month (yyyy-MM)', () => {
const date = parseDateWithYearMonth('2024', '12')
expect(isValid(date)).to.be.true
expect(format(date, 'yyyy-MM')).to.equal('2024-12')
})

it('should handle invalid year (non-numeric)', () => {
const date = parseDateWithYearMonth('abcd', '12', '31')
expect(isValid(date)).to.be.false
})

it('should handle invalid month (out of range)', () => {
const date = parseDateWithYearMonth('2024', '13', '31')
expect(isValid(date)).to.be.false
})

it('should handle invalid day (out of range)', () => {
const date = parseDateWithYearMonth('2024', '12', '32')
expect(isValid(date)).to.be.false
})

it('should handle missing day gracefully (assume first of month)', () => {
const date = parseDateWithYearMonth('2024', '12')
expect(isValid(date)).to.be.true
expect(format(date, 'yyyy-MM-dd')).to.equal('2024-12-01')
})

it('should handle single-digit month and day', () => {
const date = parseDateWithYearMonth('2024', '2', '9')
expect(isValid(date)).to.be.true
expect(format(date, 'yyyy-MM-dd')).to.equal('2024-02-09')
})

it('should handle valid leap year date', () => {
const date = parseDateWithYearMonth('2024', '2', '29')
expect(isValid(date)).to.be.true
expect(format(date, 'yyyy-MM-dd')).to.equal('2024-02-29')
})

it('should handle invalid non-leap year date', () => {
const date = parseDateWithYearMonth('2023', '2', '29')
expect(isValid(date)).to.be.false
})

it('should handle missing month (invalid case)', () => {
const date = parseDateWithYearMonth('2024', null, '31')
expect(isValid(date)).to.be.false
})

it('should handle missing year (invalid case)', () => {
const date = parseDateWithYearMonth(null, '12', '31')
expect(isValid(date)).to.be.false
})
})

describe('formatDateWithYearMonth', () => {
it('should format a full date (year, month, day)', () => {
const result = formatDateWithYearMonth({ year: 2025, month: 1, day: 6 })
expect(result).to.equal('2025-01-06')
})

it('should format a date when only year and month are provided', () => {
const result = formatDateWithYearMonth({ year: 2025, month: 11 })
expect(result).to.equal('2025-11')
})

it('should handle single-digit months and days', () => {
const result = formatDateWithYearMonth({ year: 2025, month: 4, day: 9 })
expect(result).to.equal('2025-04-09')
})

it('should throw an error for invalid year', () => {
expect(() =>
formatDateWithYearMonth({ year: 'invalid', month: 1, day: 1 })
).to.throw()
})

it('should throw an error for invalid month', () => {
expect(() =>
formatDateWithYearMonth({ year: 2025, month: 13, day: 1 })
).to.throw()
})

it('should throw an error for invalid day', () => {
expect(() =>
formatDateWithYearMonth({ year: 2025, month: 2, day: 30 })
).to.throw()
})

it('should handle edge case: February 29 on a leap year', () => {
const result = formatDateWithYearMonth({ year: 2024, month: 2, day: 29 })
expect(result).to.equal('2024-02-29')
})

it('should throw an error for February 29 on a non-leap year', () => {
expect(() =>
formatDateWithYearMonth({ year: 2023, month: 2, day: 29 })
).to.throw()
})
})
Loading

0 comments on commit 02a2405

Please sign in to comment.