Skip to content

Commit

Permalink
Merge pull request #32 from dhis2/add-date-validation-util
Browse files Browse the repository at this point in the history
feat(validation): add and expose validateDateString utility
  • Loading branch information
Mohammer5 authored Feb 26, 2024
2 parents b9450f3 + cb4c919 commit 039fd0b
Show file tree
Hide file tree
Showing 9 changed files with 202 additions and 24 deletions.
4 changes: 2 additions & 2 deletions src/constants/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export * from './calendars'
export * from './numberingSystems'
export { calendars } from './calendars'
export { numberingSystems } from './numberingSystems'
11 changes: 3 additions & 8 deletions src/hooks/useDatePicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { dhis2CalendarsMap } from '../constants/dhis2CalendarsMap'
import { getNowInCalendar } from '../index'
import { PickerOptions, SupportedCalendar } from '../types'
import { extractDatePartsFromDateString } from '../utils'
import { formatYyyyMmDD, getCustomCalendarIfExists } from '../utils/helpers'
import localisationHelpers from '../utils/localisationHelpers'
import { useCalendarWeekDays } from './internal/useCalendarWeekDays'
Expand Down Expand Up @@ -44,14 +45,8 @@ const fromDateParts = (date: string, options: PickerOptions) => {
let result: Temporal.PlainDateLike

try {
const dateParts = date?.split('-')
if (dateParts.length !== 3) {
throw new Error(
`Invalid date ${date} - date should be in the format YYYY-MM-DD`
)
}
const [year, month, day] = dateParts
result = { year: Number(year), month: Number(month), day: Number(day) }
const { year, month, day } = extractDatePartsFromDateString(date)
result = { year, month, day }
} catch (err) {
console.warn(err)

Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export * from './hooks'
export * as constants from './constants'
export * from './period-calculation'
export { default as getNowInCalendar } from './utils/getNowInCalendar'
export { getNowInCalendar, validateDateString } from './utils'
1 change: 1 addition & 0 deletions src/utils/date-string-regexp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const dateStringRegExp = /^(\d{4})([-./])(\d{2})(\2)(\d{2})$/
16 changes: 16 additions & 0 deletions src/utils/extract-date-parts-from-date-string.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { dateStringRegExp } from './date-string-regexp'

export function extractDatePartsFromDateString(dateString: string) {
const parts = dateString.match(dateStringRegExp)

if (!parts) {
throw new Error(`Date string is invalid, received "${dateString}"`)
}

const [, yearStr, , monthStr, , dayStr] = parts
const year = parseInt(yearStr, 10)
const month = parseInt(monthStr, 10)
const day = parseInt(dayStr, 10)

return { year, month, day }
}
15 changes: 2 additions & 13 deletions src/utils/from-date-string.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,14 @@
import { Temporal } from '@js-temporal/polyfill'
import { SupportedCalendar } from '../types'

const dateStringRegExp = /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/
import { extractDatePartsFromDateString } from './extract-date-parts-from-date-string'

type FromDateString = (args: {
date: string
calendar: SupportedCalendar
}) => Temporal.PlainDate

const fromDateString: FromDateString = ({ date, calendar }) => {
const parts = date.match(dateStringRegExp)

if (!parts) {
throw new Error(`Date string is invalid, received "${date}"`)
}

const [, yearStr, monthStr, dayStr] = parts
const year = parseInt(yearStr, 10)
const month = parseInt(monthStr, 10)
const day = parseInt(dayStr, 10)

const { year, month, day } = extractDatePartsFromDateString(date)
return Temporal.PlainDate.from({ year, month, day, calendar })
}

Expand Down
2 changes: 2 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export { extractDatePartsFromDateString } from './extract-date-parts-from-date-string'
export { default as fromAnyDate } from './from-any-date'
export { default as getNowInCalendar } from './getNowInCalendar'
export * from './helpers'
export { default as localisationHelpers } from './localisationHelpers'
export { validateDateString } from './validate-date-string'
146 changes: 146 additions & 0 deletions src/utils/validate-date-string.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { Temporal } from '@js-temporal/polyfill'

Check warning on line 1 in src/utils/validate-date-string.spec.ts

View workflow job for this annotation

GitHub Actions / lint

'Temporal' is defined but never used
import { validateDateString } from './validate-date-string'

describe('validateDateString (gregory)', () => {
it('should return undefined for a date with dashes as delimiter', () => {
expect(validateDateString('2024-02-02')).toBe(undefined)
})

it('should return undefined for a date with dashes as slashes', () => {
expect(validateDateString('2024/02/02')).toBe(undefined)
})

it('should return undefined for a date with dashes as dots', () => {
expect(validateDateString('2024.02.02')).toBe(undefined)
})

it('should return an error message for a date with mixed delimiters', () => {
expect(validateDateString('2024/02.02')).toBe(
'Date string is invalid, received "2024/02.02"'
)
})

it('should return an error message for a date missing year digits', () => {
expect(validateDateString('200.02.02')).toBe(
'Date string is invalid, received "200.02.02"'
)
})

it('should return an error message for a date missing month digits', () => {
expect(validateDateString('2000.2.02')).toBe(
'Date string is invalid, received "2000.2.02"'
)
})

it('should return an error message for a date missing day digits', () => {
expect(validateDateString('2000.02.2')).toBe(
'Date string is invalid, received "2000.02.2"'
)
})

it('should return an error message when the value is out of range', () => {
expect(validateDateString('2025-12-32')).toBe(
'value out of range: 1 <= 32 <= 31'
)
})
})

describe('validateDateString (ethiopic)', () => {
it('should return undefined for a date with dashes as delimiter', () => {
expect(validateDateString('2015-13-06', { calendar: 'ethiopic' })).toBe(
undefined
)
})

it('should return undefined for a date with dashes as slashes', () => {
expect(validateDateString('2015/13/06', { calendar: 'ethiopic' })).toBe(
undefined
)
})

it('should return undefined for a date with dashes as dots', () => {
expect(validateDateString('2015.13.06', { calendar: 'ethiopic' })).toBe(
undefined
)
})

it('should return an error message for a date with mixed delimiters', () => {
expect(validateDateString('2015.13/06', { calendar: 'ethiopic' })).toBe(
'Date string is invalid, received "2015.13/06"'
)
})

it('should return an error message for a date missing year digits', () => {
expect(validateDateString('201.13/06', { calendar: 'ethiopic' })).toBe(
'Date string is invalid, received "201.13/06"'
)
})

it('should return an error message for a date missing month digits', () => {
expect(validateDateString('201.1/06', { calendar: 'ethiopic' })).toBe(
'Date string is invalid, received "201.1/06"'
)
})

it('should return an error message for a date missing day digits', () => {
expect(validateDateString('2015.13/6', { calendar: 'ethiopic' })).toBe(
'Date string is invalid, received "2015.13/6"'
)
})

it('should return an error message when the value is out of range', () => {
expect(validateDateString('2015-14-01', { calendar: 'ethiopic' })).toBe(
'value out of range: 1 <= 14 <= 13'
)
})
})

describe('validateDateString (nepali)', () => {
it('should return undefined for a date with dashes as delimiter', () => {
expect(validateDateString('2080-10-29', { calendar: 'ethiopic' })).toBe(
undefined
)
})

it('should return undefined for a date with dashes as slashes', () => {
expect(validateDateString('2080/10/29', { calendar: 'ethiopic' })).toBe(
undefined
)
})

it('should return undefined for a date with dashes as dots', () => {
expect(validateDateString('2080.10.29', { calendar: 'ethiopic' })).toBe(
undefined
)
})

it('should return an error message for a date with mixed delimiters', () => {
expect(validateDateString('2080.10/29', { calendar: 'ethiopic' })).toBe(
'Date string is invalid, received "2080.10/29"'
)
})

it('should return an error message for a date missing year digits', () => {
expect(validateDateString('280.10.29', { calendar: 'ethiopic' })).toBe(
'Date string is invalid, received "280.10.29"'
)
})

it('should return an error message for a date missing month digits', () => {
expect(validateDateString('2080.1.29', { calendar: 'ethiopic' })).toBe(
'Date string is invalid, received "2080.1.29"'
)
})

it('should return an error message for a date missing day digits', () => {
expect(validateDateString('2080.10.9', { calendar: 'ethiopic' })).toBe(
'Date string is invalid, received "2080.10.9"'
)
})

it('should return an error message when the value is out of range', () => {
expect(validateDateString('2080.14.30', { calendar: 'ethiopic' })).toBe(
'value out of range: 1 <= 14 <= 13'
)
})
})
29 changes: 29 additions & 0 deletions src/utils/validate-date-string.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Temporal } from '@js-temporal/polyfill'
import { dhis2CalendarsMap } from '../constants/dhis2CalendarsMap'
import type { SupportedCalendar } from '../types'
import { extractDatePartsFromDateString } from './extract-date-parts-from-date-string'
import { getCustomCalendarIfExists } from './helpers'

export function validateDateString(
dateString: string,
{ calendar = 'gregory' }: { calendar?: SupportedCalendar } = {}
): undefined | string {
const resolvedCalendar = getCustomCalendarIfExists(
dhis2CalendarsMap[calendar] ?? calendar
) as SupportedCalendar

try {
// will throw if the format of the date is incorrect
const { year, month, day } = extractDatePartsFromDateString(dateString)

// will throw if the year, month or day is out of range
Temporal.PlainDate.from(
{ year, month, day, calendar: resolvedCalendar },
{ overflow: 'reject' }
)
} catch (e) {
return (e as Error).message
}

return undefined
}

0 comments on commit 039fd0b

Please sign in to comment.