Skip to content

Commit

Permalink
(feat) O3-3684: Improve duration representation of age function (#1100
Browse files Browse the repository at this point in the history
)
  • Loading branch information
chibongho authored Jul 31, 2024
1 parent 560741f commit e9428a9
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 142 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
},
"packageManager": "yarn@4.2.2",
"dependencies": {
"@formatjs/intl-durationformat": "^0.2.4",
"@hookform/resolvers": "^3.6.0",
"react-hook-form": "^7.52.0",
"zod": "^3.23.8"
Expand Down
63 changes: 8 additions & 55 deletions packages/framework/esm-framework/docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,13 +227,11 @@

- [age](API.md#age)
- [canAccessStorage](API.md#canaccessstorage)
- [daysIntoYear](API.md#daysintoyear)
- [displayName](API.md#displayname)
- [formatPatientName](API.md#formatpatientname)
- [formattedName](API.md#formattedname)
- [getDefaultsFromConfigSchema](API.md#getdefaultsfromconfigschema)
- [getPatientName](API.md#getpatientname)
- [isSameDay](API.md#issameday)
- [isVersionSatisfied](API.md#isversionsatisfied)
- [retry](API.md#retry)
- [selectPreferredName](API.md#selectpreferredname)
Expand Down Expand Up @@ -6519,15 +6517,19 @@ ___

### age

**age**(`dateString`): `string`
**age**(`birthDate`, `currentDate?`): `string`

Gets a human readable and locale supported age represention of the provided date string.
Gets a human readable and locale supported representation of a person's age, given their birthDate,
The representation logic follows the guideline here:
https://webarchive.nationalarchives.gov.uk/ukgwa/20160921162509mp_/http://systems.digital.nhs.uk/data/cui/uig/patben.pdf
(See Tables 7 and 8)

#### Parameters

| Name | Type | Description |
| :------ | :------ | :------ |
| `dateString` | `string` | The stringified date. |
| `birthDate` | `undefined` \| ``null`` \| `string` \| `number` \| `Date` \| `Dayjs` | The birthDate. |
| `currentDate` | `undefined` \| ``null`` \| `string` \| `number` \| `Date` \| `Dayjs` | Optional. If provided, calculates the age of the person at the provided currentDate (instead of now). |

#### Returns

Expand All @@ -6537,7 +6539,7 @@ A human-readable string version of the age.

#### Defined in

[packages/framework/esm-utils/src/age-helpers.ts:36](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-utils/src/age-helpers.ts#L36)
[packages/framework/esm-utils/src/age-helpers.ts:17](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-utils/src/age-helpers.ts#L17)

___

Expand Down Expand Up @@ -6567,30 +6569,6 @@ True if the WebStorage API object is able to be accessed, false otherwise

___

### daysIntoYear

**daysIntoYear**(`date`): `number`

Gets the number of days in the year of the given date.

#### Parameters

| Name | Type | Description |
| :------ | :------ | :------ |
| `date` | `Date` | The date to compute the days within the year. |

#### Returns

`number`

The number of days.

#### Defined in

[packages/framework/esm-utils/src/age-helpers.ts:9](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-utils/src/age-helpers.ts#L9)

___

### displayName

**displayName**(`patient`): `string`
Expand Down Expand Up @@ -6721,31 +6699,6 @@ The patient's display name or an empty string if name is not present.

___

### isSameDay

**isSameDay**(`firstDate`, `secondDate`): `boolean`

Checks if two dates are representing the same day.

#### Parameters

| Name | Type | Description |
| :------ | :------ | :------ |
| `firstDate` | `Date` | The first date. |
| `secondDate` | `Date` | The second date. |

#### Returns

`boolean`

True if both are located on the same day.

#### Defined in

[packages/framework/esm-utils/src/age-helpers.ts:25](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-utils/src/age-helpers.ts#L25)

___

### isVersionSatisfied

**isVersionSatisfied**(`requiredVersion`, `installedVersion`): `boolean`
Expand Down
37 changes: 37 additions & 0 deletions packages/framework/esm-utils/src/age-helpers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import dayjs from 'dayjs';
import type { i18n } from 'i18next';
import { age } from '.';

window.i18next = { language: 'en' } as i18n;

describe('Age Helper', () => {
// test cases mostly taken from
// https://webarchive.nationalarchives.gov.uk/ukgwa/20160921162509mp_/http://systems.digital.nhs.uk/data/cui/uig/patben.pdf
// (Table 8)
const now = dayjs('2024-07-30');
const test1 = now.subtract(1, 'hour').subtract(30, 'minutes');
const test2 = now.subtract(1, 'day').subtract(2, 'hours').subtract(5, 'minutes');
const test3 = now.subtract(3, 'days').subtract(17, 'hours').subtract(30, 'minutes');
const test4 = now.subtract(27, 'days').subtract(5, 'hours').subtract(2, 'minutes');
const test5 = now.subtract(28, 'days').subtract(5, 'hours').subtract(2, 'minutes');
const test6 = now.subtract(29, 'days').subtract(5, 'hours').subtract(2, 'minutes');
const test7 = now.subtract(1, 'year').subtract(1, 'day').subtract(5, 'hours');
const test8 = now.subtract(1, 'year').subtract(8, 'day').subtract(5, 'hours');
const test9 = now.subtract(1, 'year').subtract(39, 'day').subtract(5, 'hours');
const test10 = now.subtract(4, 'year').subtract(39, 'day');
const test11 = now.subtract(18, 'year').subtract(39, 'day');

it('should render durations correctly', () => {
expect(age(test1, now)).toBe('90 min');
expect(age(test2, now)).toBe('26 hr');
expect(age(test3, now)).toBe('3 days');
expect(age(test4, now)).toBe('27 days');
expect(age(test5, now)).toBe('4 wks');
expect(age(test6, now)).toBe('4 wks, 1 day');
expect(age(test7, now)).toBe('12 mths, 1 day');
expect(age(test8, now)).toBe('12 mths, 8 days');
expect(age(test9, now)).toBe('13 mths, 9 days');
expect(age(test10, now)).toBe('4 yrs, 1 mth');
expect(age(test11, now)).toBe('18 yrs');
});
});
128 changes: 41 additions & 87 deletions packages/framework/esm-utils/src/age-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,101 +1,55 @@
/** @module @category Utility */
import dayjs from 'dayjs';
import { getLocale } from './omrs-dates';
import { DurationFormat } from '@formatjs/intl-durationformat';
import { type DurationInput } from '@formatjs/intl-durationformat/src/types';

/**
* Gets the number of days in the year of the given date.
* @param date The date to compute the days within the year.
* @returns The number of days.
*/
export function daysIntoYear(date: Date) {
return (
(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()) - Date.UTC(date.getUTCFullYear(), 0, 0)) /
24 /
60 /
60 /
1000
);
}

/**
* Checks if two dates are representing the same day.
* @param firstDate The first date.
* @param secondDate The second date.
* @returns True if both are located on the same day.
*/
export function isSameDay(firstDate: Date, secondDate: Date) {
const firstISO = firstDate.toISOString();
const secondISO = secondDate.toISOString();
return firstISO.slice(0, firstISO.indexOf('T')) === secondISO.slice(0, secondISO.indexOf('T'));
}

/**
* Gets a human readable and locale supported age represention of the provided date string.
* @param dateString The stringified date.
* Gets a human readable and locale supported representation of a person's age, given their birthDate,
* The representation logic follows the guideline here:
* https://webarchive.nationalarchives.gov.uk/ukgwa/20160921162509mp_/http://systems.digital.nhs.uk/data/cui/uig/patben.pdf
* (See Tables 7 and 8)
*
* @param birthDate The birthDate.
* @param currentDate Optional. If provided, calculates the age of the person at the provided currentDate (instead of now).
* @returns A human-readable string version of the age.
*/
export function age(dateString: string): string {
// Different from npm packages such as https://www.npmjs.com/package/timeago
export function age(birthDate: dayjs.ConfigType, currentDate: dayjs.ConfigType = dayjs()): string {
const to = dayjs(currentDate);
const from = dayjs(birthDate);

// First calculate the age in years
const today = new Date();
const birthDate = new Date(dateString);
const monthDifference = today.getUTCMonth() - birthDate.getUTCMonth();
const dateDifference = today.getUTCDate() - birthDate.getUTCDate();
let age = today.getUTCFullYear() - birthDate.getUTCFullYear();
if (monthDifference < 0 || (monthDifference === 0 && dateDifference < 0)) {
age--;
}

// Now calculate the number of months in addition to the year's age
let monthsAgo = monthDifference >= 0 ? monthDifference : monthDifference + 12;
if (dateDifference < 0) {
monthsAgo--;
}
const hourDiff = to.diff(from, 'hours');
const dayDiff = to.diff(from, 'days');
const weekDiff = to.diff(from, 'weeks');
const monthDiff = to.diff(from, 'months');
const yearDiff = to.diff(from, 'years');

// For patients less than a year old, we calculate the number of days/weeks they have been alive
let totalDaysAgo = daysIntoYear(today) - daysIntoYear(birthDate);
if (totalDaysAgo < 0) {
totalDaysAgo += 365;
}
const weeksAgo = Math.floor(totalDaysAgo / 7);
const duration: DurationInput = {};

const locale = getLocale();

// Depending on their age, return a different representation of their age.
if (age === 0) {
if (isSameDay(today, birthDate)) {
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
return rtf.format(0, 'day');
} else if (totalDaysAgo < 31) {
const totalDaysAgoStr = new Intl.NumberFormat(locale, {
style: 'unit',
unit: 'day',
unitDisplay: 'short',
}).format(totalDaysAgo);

return totalDaysAgoStr;
} else {
const weeksAgoStr = new Intl.NumberFormat(locale, {
style: 'unit',
unit: 'week',
unitDisplay: 'short',
}).format(weeksAgo);
return weeksAgoStr;
}
} else if (age < 2) {
const monthsAgoStr = new Intl.NumberFormat(locale, {
style: 'unit',
unit: 'month',
unitDisplay: 'short',
}).format(monthsAgo + 12);

return monthsAgoStr;
if (hourDiff < 2) {
const minuteDiff = to.diff(from, 'minutes');
duration['minutes'] = minuteDiff;
} else if (dayDiff < 2) {
duration['hours'] = hourDiff;
} else if (weekDiff < 4) {
duration['days'] = dayDiff;
} else if (yearDiff < 1) {
const remainderDayDiff = to.subtract(weekDiff, 'weeks').diff(from, 'days');
duration['weeks'] = weekDiff;
duration['days'] = remainderDayDiff;
} else if (yearDiff < 2) {
const remainderDayDiff = to.subtract(monthDiff, 'months').diff(from, 'days');
duration['months'] = monthDiff;
duration['days'] = remainderDayDiff;
} else if (yearDiff < 18) {
const remainderMonthDiff = to.subtract(yearDiff, 'year').diff(from, 'months');
duration['years'] = yearDiff;
duration['months'] = remainderMonthDiff;
} else {
const yearsAgoStr = new Intl.NumberFormat(locale, {
style: 'unit',
unit: 'year',
unitDisplay: 'short',
}).format(age);
return yearsAgoStr;
duration['years'] = yearDiff;
}

return new DurationFormat(locale, { style: 'short' }).format(duration);
}
31 changes: 31 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1855,6 +1855,16 @@ __metadata:
languageName: node
linkType: hard

"@formatjs/ecma402-abstract@npm:2.0.0":
version: 2.0.0
resolution: "@formatjs/ecma402-abstract@npm:2.0.0"
dependencies:
"@formatjs/intl-localematcher": "npm:0.5.4"
tslib: "npm:^2.4.0"
checksum: 10/41543ba509ea3c7d6530d57b888115f7ca242f13462a951fae4d1d1f28bae10c999f4dea28a71d2f08366d4889a3f5276cae3a16c6f6417b841a84fd314c2234
languageName: node
linkType: hard

"@formatjs/fast-memoize@npm:2.2.0":
version: 2.2.0
resolution: "@formatjs/fast-memoize@npm:2.2.0"
Expand Down Expand Up @@ -1885,6 +1895,17 @@ __metadata:
languageName: node
linkType: hard

"@formatjs/intl-durationformat@npm:^0.2.4":
version: 0.2.4
resolution: "@formatjs/intl-durationformat@npm:0.2.4"
dependencies:
"@formatjs/ecma402-abstract": "npm:2.0.0"
"@formatjs/intl-localematcher": "npm:0.5.4"
tslib: "npm:^2.4.0"
checksum: 10/5f500409a20d18967e17ffbc222f9b4c4bf7ef08cce20023c33f06d1989c2bc4cf700d1dd1d048748d0a36c882109d5375896a4964d6700f73ec18914c6de4ba
languageName: node
linkType: hard

"@formatjs/intl-localematcher@npm:0.4.0":
version: 0.4.0
resolution: "@formatjs/intl-localematcher@npm:0.4.0"
Expand All @@ -1894,6 +1915,15 @@ __metadata:
languageName: node
linkType: hard

"@formatjs/intl-localematcher@npm:0.5.4":
version: 0.5.4
resolution: "@formatjs/intl-localematcher@npm:0.5.4"
dependencies:
tslib: "npm:^2.4.0"
checksum: 10/780cb29b42e1ea87f2eb5db268577fcdc53da52d9f096871f3a1bb78603b4ba81d208ea0b0b9bc21548797c941ce435321f62d2522795b83b740f90b0ceb5778
languageName: node
linkType: hard

"@gar/promisify@npm:^1.0.1, @gar/promisify@npm:^1.1.3":
version: 1.1.3
resolution: "@gar/promisify@npm:1.1.3"
Expand Down Expand Up @@ -3012,6 +3042,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "@openmrs/esm-core@workspace:."
dependencies:
"@formatjs/intl-durationformat": "npm:^0.2.4"
"@hookform/resolvers": "npm:^3.6.0"
"@playwright/test": "npm:1.45.3"
"@swc/core": "npm:^1.3.58"
Expand Down

0 comments on commit e9428a9

Please sign in to comment.