Skip to content

Commit

Permalink
feat: expose function to convert calendar dates to iso8601
Browse files Browse the repository at this point in the history
  • Loading branch information
kabaros committed May 27, 2024
1 parent 4e50063 commit 3806e99
Show file tree
Hide file tree
Showing 5 changed files with 204 additions and 27 deletions.
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,44 @@ it('should get today date in Ethiopic', () => {
})
```

## convertFromIso8601 and convertToIso8601

`convertFromIso8601` and `convertToIso8601` are used to convert between Iso8601 (gregorian) dates and specific calendars (i.e. Ethiopic or Nepali). It accepts either a string in the format `yyyy-MM-dd` or an object representing the date properties (`year`, `month` and `day`).

### Examples

```js
it('should convert a gregorian date to ethiopic', () => {
const result = convertFromIso8601('2024-05-23', 'ethiopic')
expect(result).toMatchObject({
year: 7516,
eraYear: 2016,
month: 9,
day: 15,
})
})
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', () => {
const result = convertToIso8601('2016-09-15', 'ethiopic')
expect(result).toMatchObject({ year: 2024, month: 5, day: 23 })
})
it('should accept a date object instead of a string', () => {
const result = convertToIso8601(
{
year: 2081,
month: 2,
day: 10,
},
'nepali'
)
expect(result).toMatchObject({ year: 2024, month: 5, day: 23 })
})
```


### Types

The method takes two positional arguments:
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export {
getNowInCalendar,
validateDateString,
convertFromIso8601,
convertToIso8601,
} from './utils'
120 changes: 97 additions & 23 deletions src/utils/convert-date.spec.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,122 @@
import { convertFromIso8601 } from './convert-date'
import { convertFromIso8601, convertToIso8601 } from '../'

describe('date conversion from gregorian', () => {
describe('to ethiopic', () => {
it('should convert a date', () => {
const result = convertFromIso8601('2024-05-23', 'ethiopic')
expect(result.eraYear).toEqual(2016)
expect(result.year).toEqual(7516)
expect(result.month).toEqual(9)
expect(result.day).toEqual(15)
expect(result).toMatchObject({
year: 7516,
eraYear: 2016,
month: 9,
day: 15,
})
})
it('should convert a date object', () => {
const result = convertFromIso8601(
{
year: 2024,
month: 5,
day: 23,
},
'ethiopic'
)
expect(result).toMatchObject({
year: 7516,
eraYear: 2016,
month: 9,
day: 15,
})
})
it('should convert a date if "ethiopian" is passed instad of "ethiopic"', () => {
const result = convertFromIso8601('2024-05-23', 'ethiopian' as any)

Check warning on line 31 in src/utils/convert-date.spec.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
expect(result.eraYear).toEqual(2016)
expect(result.year).toEqual(7516)
expect(result.month).toEqual(9)
expect(result.day).toEqual(15)
expect(result).toMatchObject({
year: 7516,
eraYear: 2016,
month: 9,
day: 15,
})
})
})
describe('to nepali', () => {
it('should convert a date', () => {
const result = convertFromIso8601('2024-05-23', 'nepali')
expect(result.eraYear).toEqual(2081)
expect(result.year).toEqual(2081)
expect(result.month).toEqual(2)
expect(result.day).toEqual(10)
expect(result).toMatchObject({
eraYear: 2081,
year: 2081,
month: 2,
day: 10,
})
})

it('should convert a date object', () => {
const result = convertFromIso8601(
{
year: 2024,
month: 5,
day: 23,
},
'nepali'
)
expect(result).toMatchObject({
eraYear: 2081,
year: 2081,
month: 2,
day: 10,
})
})
})
it('should convert to islamic date', () => {
const result = convertFromIso8601('2024-05-23', 'islamic')
expect(result.eraYear).toEqual(1445)
expect(result.year).toEqual(1445)
expect(result.month).toEqual(11)
expect(result.day).toEqual(15)
expect(result).toMatchObject({
eraYear: 1445,
year: 1445,
month: 11,
day: 15,
})
})
})

// gregorian, ethiopic and nepali

describe('date conversion from', () => {
describe('date conversion to gregorian', () => {
describe('ethiopic to gregorian', () => {
it('should convert a date', () => {})
it('should convert a date if "ethiopian" is passed instad of "ethiopic"', () => {})
it('should convert a date taking care of setting the correct era for ethiopic calendar', () => {
const result = convertToIso8601('2016-09-15', 'ethiopic')
expect(result).toMatchObject({ year: 2024, month: 5, day: 23 })
})
it('should convert a date if "ethiopian" is passed instad of "ethiopic"', () => {
const result = convertToIso8601('2016-09-15', 'ethiopian' as any)

Check warning on line 86 in src/utils/convert-date.spec.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
expect(result).toMatchObject({ year: 2024, month: 5, day: 23 })
})
it('should convert a date object', () => {
const result = convertToIso8601(
{
year: 2016,
month: 9,
day: 15,
},
'ethiopic'
)
expect(result).toMatchObject({ year: 2024, month: 5, day: 23 })
})
})
describe('nepali to gregorian', () => {
it('should convert a date', () => {})
it('should convert a date', () => {
const result = convertToIso8601('2081-02-10', 'nepali')
expect(result).toMatchObject({ year: 2024, month: 5, day: 23 })
})
it('should convert a date object', () => {
const result = convertToIso8601(
{
year: 2081,
month: 2,
day: 10,
},
'nepali'
)
expect(result).toMatchObject({ year: 2024, month: 5, day: 23 })
})
})
it('islamic to gregorian', () => {
const result = convertToIso8601('1445-11-15', 'islamic')
expect(result).toMatchObject({ year: 2024, month: 5, day: 23 })
})
})
70 changes: 67 additions & 3 deletions src/utils/convert-date.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,81 @@
import { Temporal } from '@js-temporal/polyfill'
import { dhis2CalendarsMap } from '../constants/dhis2CalendarsMap'
import { SupportedCalendar } from '../types'
import { extractDatePartsFromDateString } from './extract-date-parts-from-date-string'
import { getCustomCalendarIfExists } from './helpers'

type PlainDate = {
year: number
month: number
day: number
// keeping eraYear to be consistent with the default behaviour of Temporal (check method documentation for more info)
eraYear?: number
}

type ConvertDateFn = (
date: string | Temporal.PlainDate,
date: string | Temporal.PlainDateLike,
calendar: SupportedCalendar
) => Temporal.PlainDate
) => PlainDate

/**
* converts from an iso8601 (gregorian) date to a specific calendar
*
* @param date string in the format yyyy-MM-dd
* @param userCalendar the calendar to covert to
* @returns an object representing the date
*
* NOTE: the returned object contains two properties year and eraYear
* to be consistent with the default behaviour of Temporal. This only affects
* ethiopic calendar in practice. When accessing year, consumers should be defensive
* and do: `const yearToUse = result.eraYear ?? result.year` for example.
*
* @see https://github.com/tc39/ecma402/issues/534 for more details
*/
export const convertFromIso8601: ConvertDateFn = (date, userCalendar) => {
const calendar = getCustomCalendarIfExists(
dhis2CalendarsMap[userCalendar] ?? userCalendar
) as SupportedCalendar

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

return {
eraYear,
year,
month,
day,
}
}

/**
* converts from a specific calendar (i.e. ethiopic or nepali) to iso8601 (gregorian)
*
* @param date calendar date in the format yyyy-MM-dd
* @param userCalendar the calendar to convert from
* @returns an object representing the iso8601 date
*/
export const convertToIso8601: ConvertDateFn = (date, userCalendar) => {
const calendar = getCustomCalendarIfExists(
dhis2CalendarsMap[userCalendar] ?? userCalendar
) as SupportedCalendar

const dateParts: Temporal.PlainDateLike =
typeof date === 'string' ? extractDatePartsFromDateString(date) : date

// this is a workaround for the ethiopic calendar being in a different
// era by default. There is a discussion on Temporal on which should be
// considered the default era. For us, we need to manually set it to era1
// https://github.com/js-temporal/temporal-polyfill/blob/8fd0dead40de7c31398f4d2d41e145466ca57a16/lib/calendar.ts#L2010
if (calendar === 'ethiopic') {
dateParts.eraYear = dateParts.year
dateParts.era = 'era1'
delete dateParts.year
}

dateParts.calendar = calendar

const { year, month, day } =
Temporal.PlainDate.from(dateParts).withCalendar('iso8601')

return { year, month, day }
}
2 changes: 1 addition & 1 deletion src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ export * from './helpers'
export { default as localisationHelpers } from './localisationHelpers'
export { validateDateString } from './validate-date-string'

export { convertFromIso8601 } from './convert-date'
export { convertFromIso8601, convertToIso8601 } from './convert-date'

0 comments on commit 3806e99

Please sign in to comment.