-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Use start dates from API for "expected in" message (#151)
## Description Having finally gotten our dates story straight on the API side, we can now just use them here in the frontend. The old frontend just gets years from v0, so it displays those as-is if they're in the future. The new frontend determines whether the pseudo-ISO-8601 date is in the future, and displays the year from it if so. (In the interest of time, I didn't want to go further in this PR by rendering "2024H2" as "late 2024" or whatever, though we can easily go that route later.) I also added Jest setup so I could write a unit test for the date logic. I don't know how much other unit testing we'll need in this codebase, but now the infrastructure is there for when we want it. ## Test Plan New unit test for the date parsing. (Useful, because JS's Date class is a nightmare to work with!) Set my local API instance as the API host (`api-host` attribute on the calculator component). Look at the old frontend and make sure all the IRA rebates show up with "2025". Look at the new frontend with a low-ish income, and make sure the IRA rebates show up with "Expected in 2025", and they are shown after all other incentives in their item category.
- Loading branch information
Showing
11 changed files
with
1,627 additions
and
57 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
/** | ||
* For a detailed explanation regarding each configuration property, visit: | ||
* https://jestjs.io/docs/configuration | ||
*/ | ||
|
||
import type { Config } from 'jest'; | ||
|
||
const config: Config = { | ||
// A preset that is used as a base for Jest's configuration | ||
preset: 'ts-jest', | ||
}; | ||
|
||
export default config; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
/** | ||
* Returns whether the given API date is unambiguously in the future, relative | ||
* to "now". (start_date and end_date in the API can represent a range of dates | ||
* rather than just a single one.) | ||
* | ||
* The [now] param will be interpreted in local time. | ||
*/ | ||
export function isInFuture(apiDate: string, now: Date): boolean { | ||
// Construct the timestamp at UTC midnight on the earliest possible day that | ||
// the API date refers to. | ||
let earliestPossibleInstant: number; | ||
const match = apiDate.match(/^(\d{4})(Q|H)(\d)$/); | ||
if (match) { | ||
// Quarter or half | ||
const parsedYear = parseInt(match[1]); | ||
const halfOrQuarterNumber = parseInt(match[3]); | ||
// Date.getMonth is 0-based, so the first month of Q1 is 0, first month of | ||
// Q2 is 3, and so on. | ||
const firstPossibleMonth = | ||
match[2] === 'Q' | ||
? (halfOrQuarterNumber - 1) * 3 | ||
: (halfOrQuarterNumber - 1) * 6; | ||
earliestPossibleInstant = Date.UTC(parsedYear, firstPossibleMonth, 1); | ||
} else { | ||
// It's year, year-month, or year-month-day. | ||
const parts = apiDate.split('-'); | ||
const parsedYear = parseInt(parts[0]); | ||
const parsedMonth = parts[1] ? parseInt(parts[1]) - 1 : 0; // 0-based month | ||
const parsedDay = parts[2] ? parseInt(parts[2]) : 1; | ||
earliestPossibleInstant = Date.UTC(parsedYear, parsedMonth, parsedDay); | ||
} | ||
|
||
// Construct the timestamp at UTC midnight, with the Y/M/D of now, as | ||
// interpreted in local time. This avoids timezone issues by only comparing | ||
// timestamps with the same time and timezone component. | ||
const utcNow = Date.UTC(now.getFullYear(), now.getMonth(), now.getDate()); | ||
return utcNow < earliestPossibleInstant; | ||
} | ||
|
||
export function getYear(apiDate: string): number { | ||
return parseInt(apiDate.slice(0, 4)); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import { describe, expect, test } from '@jest/globals'; | ||
import { isInFuture } from '../../src/api/dates'; | ||
|
||
/** Turn an ISO 8601 date string into a Date in the local timezone. */ | ||
function localDate(dateStr: string): Date { | ||
// If you just pass a date to the Date constructor, it will get a time of | ||
// midnight UTC, but isInFuture looks at it in local time. So construct a | ||
// Date of the current instant and replace its year/month/day components, so | ||
// that isInFuture will see the same year/month/day we see here. | ||
const date = new Date(); | ||
const [year, month, day] = dateStr.split('-'); | ||
date.setFullYear(parseInt(year)); | ||
date.setMonth(parseInt(month) - 1); // Date's months are 0-based | ||
date.setDate(parseInt(day)); | ||
return date; | ||
} | ||
|
||
describe('quarters', () => { | ||
test.each([ | ||
{ date: '2024Q4', now: '2024-09-30' }, | ||
{ date: '2025Q1', now: '2024-12-31' }, | ||
])('$date should be in future on $now', ({ date, now }) => { | ||
expect(isInFuture(date, localDate(now))).toBe(true); | ||
}); | ||
test.each([ | ||
{ date: '2023Q4', now: '2024-01-01' }, | ||
{ date: '2024Q4', now: '2024-10-01' }, | ||
{ date: '2024Q1', now: '2024-10-01' }, | ||
])('$date should not be in future on $now', ({ date, now }) => { | ||
expect(isInFuture(date, localDate(now))).toBe(false); | ||
}); | ||
}); | ||
|
||
describe('halves', () => { | ||
test.each([ | ||
{ date: '2024H2', now: '2024-01-30' }, | ||
{ date: '2024H2', now: '2024-06-30' }, | ||
{ date: '2025H1', now: '2024-06-30' }, | ||
])('$date should be in future on $now', ({ date, now }) => { | ||
expect(isInFuture(date, localDate(now))).toBe(true); | ||
}); | ||
test.each([ | ||
{ date: '2024H2', now: '2024-07-01' }, | ||
{ date: '2024H2', now: '2024-12-31' }, | ||
{ date: '2024H1', now: '2024-06-30' }, | ||
])('$date should not be in future on $now', ({ date, now }) => { | ||
expect(isInFuture(date, localDate(now))).toBe(false); | ||
}); | ||
}); | ||
|
||
describe('years', () => { | ||
test.each([ | ||
{ date: '2025', now: '2024-01-01' }, | ||
{ date: '2025', now: '2024-12-31' }, | ||
])('$date should be in future on $now', ({ date, now }) => { | ||
expect(isInFuture(date, localDate(now))).toBe(true); | ||
}); | ||
test.each([ | ||
{ date: '2023', now: '2024-01-01' }, | ||
{ date: '2024', now: '2024-01-01' }, | ||
{ date: '2024', now: '2024-12-31' }, | ||
])('$date should not be in future on $now', ({ date, now }) => { | ||
expect(isInFuture(date, localDate(now))).toBe(false); | ||
}); | ||
}); | ||
|
||
describe('year-months', () => { | ||
test.each([ | ||
{ date: '2024-12', now: '2024-06-30' }, | ||
{ date: '2024-12', now: '2024-11-30' }, | ||
{ date: '2025-01', now: '2024-12-31' }, | ||
])('$date should be in future on $now', ({ date, now }) => { | ||
expect(isInFuture(date, localDate(now))).toBe(true); | ||
}); | ||
test.each([ | ||
{ date: '2023-12', now: '2024-01-01' }, | ||
{ date: '2024-01', now: '2024-01-01' }, | ||
{ date: '2024-01', now: '2024-12-31' }, | ||
])('$date should not be in future on $now', ({ date, now }) => { | ||
expect(isInFuture(date, localDate(now))).toBe(false); | ||
}); | ||
}); | ||
|
||
describe('year-month-days', () => { | ||
test.each([ | ||
{ date: '2025-01-01', now: '2024-12-31' }, | ||
{ date: '2024-01-02', now: '2024-01-01' }, | ||
{ date: '2024-02-02', now: '2024-01-02' }, | ||
])('$date should be in future on $now', ({ date, now }) => { | ||
expect(isInFuture(date, localDate(now))).toBe(true); | ||
}); | ||
test.each([ | ||
{ date: '2023-12-31', now: '2024-01-01' }, | ||
{ date: '2024-01-02', now: '2024-01-02' }, | ||
{ date: '2024-01-01', now: '2024-02-02' }, | ||
])('$date should not be in future on $now', ({ date, now }) => { | ||
expect(isInFuture(date, localDate(now))).toBe(false); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.