From 77ae2cb7d786f20262b2fe9fbd3e4d8a0cc7a071 Mon Sep 17 00:00:00 2001 From: Amy Yeung Date: Wed, 24 Jun 2026 11:44:50 -0400 Subject: [PATCH 1/2] Fix flaky getAgeData calculations by refactoring out division calculation --- src/utils.js | 28 ++++++++------------- test/utils.spec.js | 63 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 17 deletions(-) diff --git a/src/utils.js b/src/utils.js index 28fed08..dfdae46 100644 --- a/src/utils.js +++ b/src/utils.js @@ -182,30 +182,24 @@ export const getAgeData = (birthMonth, birthYear, age, ageMonths) => { const ageData = { age: yearsOld, ageMonths: ageM, + birthYear: by, + birthMonth: bm, }; if (bm && by) { - ageData.birthMonth = bm; - ageData.birthYear = by; - - const birthDate = new Date(by, bm - 1, currDate.getDate()); - const decimalYear = (currDate - birthDate) / msPerYear; - ageData.age = Math.floor(decimalYear); - ageData.ageMonths = ageM || Math.floor(decimalYear * 12); - } else if (by) { - ageData.birthYear = by; - ageData.birthMonth = currDate.getMonth() + 1; - - const birthDate = new Date(by, ageData.birthMonth - 1, currDate.getDate()); - const decimalYear = (currDate - birthDate) / msPerYear; - ageData.age = Math.floor(decimalYear); - ageData.ageMonths = ageM || Math.floor(decimalYear * 12); + const ageMonthDiff = ((currDate.getFullYear() - by) * 12) + (currDate.getMonth() + 1 - bm); + ageData.age = Math.floor(ageMonthDiff / 12); + ageData.ageMonths = ageMonthDiff; } else if (ageM) { const birthDate = new Date(); - birthDate.setMonth(birthDate.getMonth() - ageM); + birthDate.setMonth(currDate.getMonth() - ageM); ageData.birthYear = birthDate.getFullYear(); ageData.birthMonth = birthDate.getMonth() + 1; - ageData.age = Math.floor((currDate - birthDate) / msPerYear); + ageData.age = Math.floor(ageM / 12); + } else if (by) { + ageData.birthMonth = currDate.getMonth() + 1; + ageData.age = currDate.getFullYear() - by; + ageData.ageMonths = ageData.age * 12; } else if (yearsOld) { ageData.birthYear = currDate.getFullYear() - yearsOld; ageData.birthMonth = currDate.getMonth() + 1; diff --git a/test/utils.spec.js b/test/utils.spec.js index 5b55231..0ec93ea 100644 --- a/test/utils.spec.js +++ b/test/utils.spec.js @@ -325,6 +325,69 @@ test('Sets the correct age fields for all possible inputs', () => { jest.useRealTimers(); }); +describe('Edge cases for getAgeData', () => { + beforeEach(() => { + jest.useFakeTimers(); + jest.setSystemTime(new Date('2024-06-15T12:00:00Z')); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + test('Birthday has not happened yet this year (future month)', () => { + const result = getAgeData(12, 2020, null, null); + expect(result.birthMonth).toBe(12); + expect(result.birthYear).toBe(2020); + expect(result.age).toBe(3); // Not 4, because December hasn't arrived + expect(result.ageMonths).toBe(42); // (2024-2020)*12 + (6-12) = 48-6 = 42 + }); + + test('Birthday is same month as current date', () => { + const result = getAgeData(6, 2020, null, null); + expect(result.birthMonth).toBe(6); + expect(result.birthYear).toBe(2020); + expect(result.age).toBe(4); + expect(result.ageMonths).toBe(48); // Exactly 4 years + }); + + test('Very young child (less than 12 months)', () => { + const result = getAgeData(null, null, null, 8); + expect(result.age).toBe(0); // 8 months = 0 years + expect(result.ageMonths).toBe(8); + expect(result.birthYear).toBe(2023); + expect(result.birthMonth).toBe(10); // June (6) - 8 months = October previous year + }); + + test('birthMonth/birthYear takes priority over ageMonths when both provided', () => { + const result = getAgeData(6, 2020, null, 50); + expect(result.birthMonth).toBe(6); + expect(result.birthYear).toBe(2020); + expect(result.age).toBe(4); // Calculated from birth date + expect(result.ageMonths).toBe(48); // Calculated from birth date, ignoring provided 50 + }); + + test('Exactly 1 year old', () => { + const result = getAgeData(6, 2023, null, null); + expect(result.age).toBe(1); + expect(result.ageMonths).toBe(12); + }); + + test('Newborn (0 months treated as null)', () => { + const result = getAgeData(null, null, null, 0); + expect(result.age).toBe(null); // 0 is treated as null by safeNumber + expect(result.ageMonths).toBe(null); + }); + + test('Birth month in the past this year', () => { + const result = getAgeData(1, 2020, null, null); + expect(result.birthMonth).toBe(1); + expect(result.birthYear).toBe(2020); + expect(result.age).toBe(4); // Birthday already happened this year + expect(result.ageMonths).toBe(53); // (2024-2020)*12 + (6-1) = 48+5 = 53 + }); +}); + test('Correctly parses grade', () => { // Test with default gradeMin and gradeMax expect(getGrade('K')).toBe(0); From 3ca5d2827c1848be2f656ec27aa12936219bd70d Mon Sep 17 00:00:00 2001 From: Amy Yeung Date: Fri, 26 Jun 2026 12:17:51 -0400 Subject: [PATCH 2/2] Add regression tests and fix birthMonth calculations in ageMonths branch --- src/utils.js | 11 +++---- test/utils.spec.js | 71 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 7 deletions(-) diff --git a/src/utils.js b/src/utils.js index dfdae46..a24508b 100644 --- a/src/utils.js +++ b/src/utils.js @@ -144,7 +144,7 @@ export function getAssetType(asset) { /** * Calculates and returns age data based on the provided birth month, birth year, - * age, and age in months. + * age, and age in months. It prioritizes the most specific data available. * * @function getAgeData * @@ -165,8 +165,6 @@ export function getAssetType(asset) { */ export const getAgeData = (birthMonth, birthYear, age, ageMonths) => { - // milliseconds per year (accounting for leap years) - const msPerYear = 1000 * 60 * 60 * 24 * 365.25; const currDate = new Date(); const safeNumber = (value) => { @@ -191,10 +189,9 @@ export const getAgeData = (birthMonth, birthYear, age, ageMonths) => { ageData.age = Math.floor(ageMonthDiff / 12); ageData.ageMonths = ageMonthDiff; } else if (ageM) { - const birthDate = new Date(); - birthDate.setMonth(currDate.getMonth() - ageM); - ageData.birthYear = birthDate.getFullYear(); - ageData.birthMonth = birthDate.getMonth() + 1; + const totalMonths = (currDate.getFullYear() * 12 + currDate.getMonth()) - ageM; + ageData.birthYear = Math.floor(totalMonths / 12); + ageData.birthMonth = (totalMonths % 12) + 1; ageData.age = Math.floor(ageM / 12); } else if (by) { ageData.birthMonth = currDate.getMonth() + 1; diff --git a/test/utils.spec.js b/test/utils.spec.js index 0ec93ea..3f3aa57 100644 --- a/test/utils.spec.js +++ b/test/utils.spec.js @@ -388,6 +388,77 @@ describe('Edge cases for getAgeData', () => { }); }); +describe('Integer division correctness (float drift regression)', () => { + afterEach(() => { + jest.useRealTimers(); + }); + + // The old msPerYear path could yield decimalYear=3.9999... on an exact anniversary, + // causing Math.floor to return 3 instead of 4. + test('Exact anniversary month returns correct whole-year age, not floor of float drift', () => { + jest.useFakeTimers(); + // Use end-of-day to maximize ms elapsed: old msPerYear path yields ~47.9999 here, + // which Math.floor would have truncated to 3 instead of 4. + jest.setSystemTime(new Date('2024-06-15T23:59:59.998Z')); + const result = getAgeData(6, 2020, null, null); // 48 months exactly + expect(result.age).toBe(4); + expect(result.ageMonths).toBe(48); + }); + + // Boundary tests + test('One month before birthday returns age minus 1, not the coming year', () => { + jest.useFakeTimers(); + jest.setSystemTime(new Date('2024-05-15T23:59:59.998Z')); + const result = getAgeData(6, 2020, null, null); // 47 months + expect(result.age).toBe(3); + expect(result.ageMonths).toBe(47); + }); + + test('One month after birthday still returns same year age', () => { + jest.useFakeTimers(); + jest.setSystemTime(new Date('2024-07-15T23:59:59.998Z')); + const result = getAgeData(6, 2020, null, null); // 49 months + expect(result.age).toBe(4); + expect(result.ageMonths).toBe(49); + }); + + // Old code: new Date(by, currMonth, currDate.getDate()) with currDate = Feb 29 and a + // non-leap birth year caused JS to silently overflow Feb 29 → March 1, shifting the + // birth date into the wrong month and producing age=4 instead of 5. + test('Feb 29 current date with non-leap birth year (birthYear only) returns correct age', () => { + jest.useFakeTimers(); + jest.setSystemTime(new Date('2024-02-29T23:59:59.998Z')); + const result = getAgeData(null, 2019, null, null); + expect(result.birthYear).toBe(2019); + expect(result.birthMonth).toBe(2); // defaults to current month + expect(result.age).toBe(5); // 2024 - 2019; old code gave 4 due to March 1 overflow + expect(result.ageMonths).toBe(60); + }); + + // Basic ageMonths-only path + test('ageMonths only returns correct age, birthMonth, and birthYear', () => { + jest.useFakeTimers(); + jest.setSystemTime(new Date('2024-06-15T23:59:59.998Z')); + const result = getAgeData(null, null, null, 18); + expect(result.age).toBe(1); + expect(result.ageMonths).toBe(18); + expect(result.birthMonth).toBe(12); // June - 18 months = December + expect(result.birthYear).toBe(2022); + }); + + // setMonth overflow: today is Oct 31, subtracting 13 months should give September 2023, + // but setMonth(-4) on a date with day=31 overflows Sep 31 → October 1, 2023. + test('ageMonths=13 on Oct 31 gives birthMonth September, not October from setMonth overflow', () => { + jest.useFakeTimers(); + jest.setSystemTime(new Date('2024-10-31T23:59:59.998Z')); + const result = getAgeData(null, null, null, 13); + expect(result.age).toBe(1); + expect(result.ageMonths).toBe(13); + expect(result.birthMonth).toBe(9); // September, not October + expect(result.birthYear).toBe(2023); + }); +}); + test('Correctly parses grade', () => { // Test with default gradeMin and gradeMax expect(getGrade('K')).toBe(0);