Skip to content

Commit

Permalink
feat: support min & max date, DD-MM-YYYY date format (#36)
Browse files Browse the repository at this point in the history
* feat: support minDate & maxDate

* fix: return `format` from extractDatePartsFromDateString

* chore: expose validatiation from useDatePicker hook

* chore: refactor code

* feat: add nepali date validation

* fix: linter issues

* fix: add types

Co-authored-by: @kabaros

* chore: add missing tests
  • Loading branch information
alaa-yahia authored Jun 9, 2024
1 parent 36a0b62 commit 3b2e57e
Show file tree
Hide file tree
Showing 16 changed files with 562 additions and 184 deletions.
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ it('should convert a Nepali date to gregorian', () => {
const result = convertToIso8601('2081-02-10', 'nepali')
expect(result).toMatchObject({ year: 2024, month: 5, day: 23 })
})
it('should convert an ethiopic date to gregorian', () => {
it('should convert an ethiopic date to gregorian', () => {
const result = convertToIso8601('2016-09-15', 'ethiopic')
expect(result).toMatchObject({ year: 2024, month: 5, day: 23 })
})
Expand All @@ -286,7 +286,6 @@ it('should accept a date object instead of a string', () => {
})
```


### Types

The method takes two positional arguments:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"access": "public"
},
"dependencies": {
"@js-temporal/polyfill": "^0.4.2",
"@js-temporal/polyfill": "0.4.3",
"classnames": "^2.3.2"
},
"peerDependencies": {
Expand Down
16 changes: 3 additions & 13 deletions src/custom-calendars/nepaliCalendar.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Temporal } from '@js-temporal/polyfill'
import { NEPALI_CALENDAR_DATA } from './nepaliCalendarData'
type CalendarYMD = { year: number; month: number; day: number }
type AssignmentOptions = { overflow?: 'constrain' | 'reject' }

/**
* https://tc39.es/proposal-temporal/docs/calendar.html
Expand Down Expand Up @@ -65,32 +64,23 @@ class NepaliCalendar extends Temporal.Calendar {
*
* A custom implementation of these methods is used to convert the calendar-space arguments to the ISO calendar.
*/
dateFromFields(
fields: CalendarYMD,
options?: AssignmentOptions
): Temporal.PlainDate {
dateFromFields(fields: CalendarYMD): Temporal.PlainDate {
const { year, day, month } = _nepaliToIso({
year: fields.year,
month: fields.month,
day: fields.day,
})
return new Temporal.PlainDate(year, month, day, this)
}
yearMonthFromFields(
fields: CalendarYMD,
options?: AssignmentOptions
): Temporal.PlainYearMonth {
yearMonthFromFields(fields: CalendarYMD): Temporal.PlainYearMonth {
const { year, day, month } = _nepaliToIso({
year: fields.year,
month: fields.month,
day: fields.day,
})
return new Temporal.PlainYearMonth(year, month, this, day)
}
monthDayFromFields(
fields: CalendarYMD,
options?: AssignmentOptions
): Temporal.PlainMonthDay {
monthDayFromFields(fields: CalendarYMD): Temporal.PlainMonthDay {
const { year, day, month } = _nepaliToIso({
year: fields.year,
month: fields.month,
Expand Down
118 changes: 56 additions & 62 deletions src/hooks/useDatePicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ 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 {
formatDate,
getCustomCalendarIfExists,
extractAndValidateDateString,
} from '../utils/helpers'
import localisationHelpers from '../utils/localisationHelpers'
import { useCalendarWeekDays } from './internal/useCalendarWeekDays'
import {
Expand All @@ -24,6 +27,10 @@ type DatePickerOptions = {
calendarDate: Temporal.ZonedDateTime
calendarDateString: string
}) => void
minDate?: string
maxDate?: string
format?: string
validation?: string
}

export type UseDatePickerReturn = UseNavigationReturnType & {
Expand All @@ -37,51 +44,31 @@ export type UseDatePickerReturn = UseNavigationReturnType & {
isToday: boolean
isInCurrentMonth: boolean
}[][]
isValid: boolean
warningMessage: string
errorMessage: string
}

type UseDatePickerHookType = (options: DatePickerOptions) => UseDatePickerReturn

const fromDateParts = (date: string, options: PickerOptions) => {
let result: Temporal.PlainDateLike

try {
const { year, month, day } = extractDatePartsFromDateString(date)
result = { year, month, day }
} catch (err) {
console.warn(err)

const { year, month, day } = getNowInCalendar(
options.calendar,
options.timeZone
)

result = { year, month, day }
}

// for ethiopic, we need to make sure it's the correct era
// there is a discussion in the Temporal proposal whether this
// should be made the default era, for now this is a workaround
if (options.calendar === 'ethiopic') {
result.era = 'era1'
result.eraYear = result.year
delete result.year
}
return result
}
export const useDatePicker: UseDatePickerHookType = ({
onDateSelect,
date: dateParts,
date: dateString,
minDate,
maxDate,
format,
validation,
options,
}) => {
const calendar = getCustomCalendarIfExists(
dhis2CalendarsMap[options.calendar!] ?? options.calendar
dhis2CalendarsMap[options.calendar ?? 'gregorian'] ?? options.calendar
) as SupportedCalendar

const resolvedOptions = useResolvedLocaleOptions({
...options,
calendar,
})
const prevDateStringRef = useRef(dateParts)
const prevDateStringRef = useRef(dateString)

const todayZdt = useMemo(
() =>
Expand All @@ -92,13 +79,23 @@ export const useDatePicker: UseDatePickerHookType = ({
[resolvedOptions]
)

const date = dateParts
? (fromDateParts(
dateParts,
resolvedOptions
) as Temporal.YearOrEraAndEraYear &
Temporal.MonthOrMonthCode & { day: number })
: todayZdt
const result = extractAndValidateDateString(dateString, {
...resolvedOptions,
minDateString: minDate,
maxDateString: maxDate,
validation: validation,
})

const date = result as Temporal.YearOrEraAndEraYear &
Temporal.MonthOrMonthCode & {
day: number
isValid: boolean
warningMessage: string
errorMessage: string
format?: string
}

date.format = !date.format ? format : date.format

const temporalCalendar = useMemo(
() => Temporal.Calendar.from(resolvedOptions.calendar),
Expand All @@ -109,17 +106,13 @@ export const useDatePicker: UseDatePickerHookType = ({
[resolvedOptions]
)

const selectedDateZdt = useMemo(
() =>
date
? Temporal.Calendar.from(temporalCalendar)
.dateFromFields(date)
.toZonedDateTime({
timeZone: temporalTimeZone,
})
: null,
[date, temporalTimeZone, temporalCalendar]
)
const selectedDateZdt = dateString
? Temporal.Calendar.from(temporalCalendar)
.dateFromFields(date)
.toZonedDateTime({
timeZone: temporalTimeZone,
})
: null

const [firstZdtOfVisibleMonth, setFirstZdtOfVisibleMonth] = useState(() => {
const zdt = selectedDateZdt || todayZdt
Expand Down Expand Up @@ -148,23 +141,19 @@ export const useDatePicker: UseDatePickerHookType = ({
(zdt: Temporal.ZonedDateTime) => {
onDateSelect({
calendarDate: zdt,
calendarDateString: formatYyyyMmDD(zdt),
calendarDateString: formatDate(zdt, undefined, date.format),
})
},
[onDateSelect]
[onDateSelect, date.format]
)
const calendarWeekDaysZdts = useCalendarWeekDays(firstZdtOfVisibleMonth)

useEffect(() => {
if (dateParts === prevDateStringRef.current) {
if (dateString === prevDateStringRef.current) {
return
}

prevDateStringRef.current = dateParts

if (!dateParts) {
return
}
prevDateStringRef.current = dateString

const zdt = Temporal.Calendar.from(temporalCalendar)
.dateFromFields(date)
Expand All @@ -183,7 +172,7 @@ export const useDatePicker: UseDatePickerHookType = ({
}
}, [
date,
dateParts,
dateString,
firstZdtOfVisibleMonth,
calendarWeekDaysZdts,
temporalCalendar,
Expand All @@ -193,15 +182,17 @@ export const useDatePicker: UseDatePickerHookType = ({
calendarWeekDays: calendarWeekDaysZdts.map((week) =>
week.map((weekDayZdt) => ({
zdt: weekDayZdt,
calendarDate: formatYyyyMmDD(weekDayZdt),
calendarDate: formatDate(weekDayZdt, undefined, date.format),
label: localisationHelpers.localiseWeekLabel(
weekDayZdt.withCalendar(localeOptions.calendar),
localeOptions
),
onClick: () => selectDate(weekDayZdt),
isSelected: selectedDateZdt
?.withCalendar('iso8601')
.equals(weekDayZdt.withCalendar('iso8601')),
? selectedDateZdt
?.withCalendar('iso8601')
.equals(weekDayZdt.withCalendar('iso8601'))
: false,
isToday: todayZdt && weekDayZdt.equals(todayZdt),
isInCurrentMonth:
firstZdtOfVisibleMonth &&
Expand All @@ -210,5 +201,8 @@ export const useDatePicker: UseDatePickerHookType = ({
),
...navigation,
weekDayLabels,
isValid: date.isValid,
warningMessage: date.warningMessage,
errorMessage: date.errorMessage,
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Temporal } from '@js-temporal/polyfill'
import { SupportedCalendar } from '../../types'
import { formatYyyyMmDD, localisationHelpers } from '../../utils/index'
import { formatDate, localisationHelpers } from '../../utils/index'
import { FixedPeriod } from '../types'

const { localiseDateLabel } = localisationHelpers
Expand Down Expand Up @@ -31,9 +31,9 @@ const buildDailyFixedPeriod: BuildDailyFixedPeriod = ({
id: value,
iso: value,
displayName,
name: formatYyyyMmDD(date),
startDate: formatYyyyMmDD(date),
endDate: formatYyyyMmDD(date),
name: formatDate(date),
startDate: formatDate(date),
endDate: formatDate(date),
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Temporal } from '@js-temporal/polyfill'
import { SupportedCalendar } from '../../types'
import { fromAnyDate, formatYyyyMmDD, padWithZeroes } from '../../utils/index'
import { fromAnyDate, formatDate, padWithZeroes } from '../../utils/index'
import { FixedPeriod, PeriodType } from '../types'
import doesPeriodEndBefore from './does-period-end-before'

Expand Down Expand Up @@ -80,8 +80,8 @@ const generateFixedPeriodsWeekly: GenerateFixedPeriodsWeekly = ({
iso: value,
name,
displayName: name,
startDate: formatYyyyMmDD(date),
endDate: formatYyyyMmDD(endofWeek),
startDate: formatDate(date),
endDate: formatDate(endofWeek),
})
}
date = fromAnyDate({ date: endofWeek, calendar }).add({ days: 1 })
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Temporal } from '@js-temporal/polyfill'
import { SupportedCalendar } from '../../types'
import {
formatYyyyMmDD,
formatDate,
isCustomCalendar,
padWithZeroes,
} from '../../utils/helpers'
Expand Down Expand Up @@ -76,8 +76,8 @@ const buildMonthlyFixedPeriod: BuildMonthlyFixedPeriod = ({
iso: id,
name,
displayName: name,
startDate: formatYyyyMmDD(month, 'startOfMonth'),
endDate: formatYyyyMmDD(endDate, 'endOfMonth'),
startDate: formatDate(month, 'startOfMonth'),
endDate: formatDate(endDate, 'endOfMonth'),
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { Temporal } from '@js-temporal/polyfill'
import { SupportedCalendar } from '../../types'
import {
fromAnyDate,
formatYyyyMmDD,
isCustomCalendar,
} from '../../utils/index'
import { fromAnyDate, formatDate, isCustomCalendar } from '../../utils/index'
import localisationHelpers from '../../utils/localisationHelpers'
import { financialYearFixedPeriodTypes } from '../period-type-groups'
import { FixedPeriod, PeriodType } from '../types'
Expand Down Expand Up @@ -47,8 +43,8 @@ const buildYearlyFixedPeriod: BuildYearlyFixedPeriod = ({
iso: value,
name,
displayName: name,
startDate: formatYyyyMmDD(startDate, 'startOfMonth'),
endDate: formatYyyyMmDD(endDate, 'endOfMonth'),
startDate: formatDate(startDate, 'startOfMonth'),
endDate: formatDate(endDate, 'endOfMonth'),
}
}

Expand Down
4 changes: 3 additions & 1 deletion src/utils/date-string-regexp.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export const dateStringRegExp = /^(\d{4})([-./])(\d{2})(\2)(\d{2})$/
//Match for dates in dd-mm-yyyy & yyyy-mm-dd formats
export const dateStringRegExp =
/^(?:(\d{4})([-./])(\d{2})(\2)(\d{2})|(\d{2})([-./])(\d{2})(\7)(\d{4}))$/
19 changes: 17 additions & 2 deletions src/utils/extract-date-parts-from-date-string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,25 @@ export function extractDatePartsFromDateString(dateString: string) {
throw new Error(`Date string is invalid, received "${dateString}"`)
}

const [, yearStr, , monthStr, , dayStr] = parts
let yearStr, monthStr, dayStr, format

if (parts[1]) {
// Match for YYYY-MM-DD
yearStr = parts[1]
monthStr = parts[3]
dayStr = parts[5]
format = 'YYYY-MM-DD'
} else {
// Match for DD-MM-YYYY
dayStr = parts[6]
monthStr = parts[8]
yearStr = parts[10]
format = 'DD-MM-YYYY'
}

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

return { year, month, day }
return { year, month, day, format }
}
Loading

0 comments on commit 3b2e57e

Please sign in to comment.