Skip to content

Commit 98e26a6

Browse files
committed
refacto date
1 parent 1691364 commit 98e26a6

4 files changed

Lines changed: 151 additions & 69 deletions

File tree

app/composables/useCharts.ts

Lines changed: 13 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { parseRepoUrl } from '#shared/utils/git-providers'
1313
import type { PackageMetaResponse } from '#shared/types'
1414
import { encodePackageName } from '#shared/utils/npm'
1515
import { fetchNpmDownloadsRange } from '~/utils/npm/api'
16+
import { parseIsoDate, toIsoDate, addDays } from '~/utils/date'
1617
import {
1718
buildDailyEvolution,
1819
buildWeeklyEvolution,
@@ -24,16 +25,6 @@ export type PackumentLikeForTime = {
2425
time?: Record<string, string>
2526
}
2627

27-
function toIsoDateString(date: Date): string {
28-
return date.toISOString().slice(0, 10)
29-
}
30-
31-
function addDays(date: Date, days: number): Date {
32-
const updatedDate = new Date(date)
33-
updatedDate.setUTCDate(updatedDate.getUTCDate() + days)
34-
return updatedDate
35-
}
36-
3728
function startOfUtcMonth(date: Date): Date {
3829
return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1))
3930
}
@@ -42,17 +33,9 @@ function startOfUtcYear(date: Date): Date {
4233
return new Date(Date.UTC(date.getUTCFullYear(), 0, 1))
4334
}
4435

45-
function parseIsoDateOnly(value: string): Date {
46-
return new Date(`${value}T00:00:00.000Z`)
47-
}
48-
49-
function formatIsoDateOnly(date: Date): string {
50-
return date.toISOString().slice(0, 10)
51-
}
52-
5336
function differenceInUtcDaysInclusive(startIso: string, endIso: string): number {
54-
const start = parseIsoDateOnly(startIso)
55-
const end = parseIsoDateOnly(endIso)
37+
const start = parseIsoDate(startIso)
38+
const end = parseIsoDate(endIso)
5639
return Math.floor((end.getTime() - start.getTime()) / 86400000) + 1
5740
}
5841

@@ -65,8 +48,8 @@ function splitIsoRangeIntoChunksInclusive(
6548
if (totalDays <= maximumDaysPerRequest) return [{ startIso, endIso }]
6649

6750
const chunks: Array<{ startIso: string; endIso: string }> = []
68-
let cursorStart = parseIsoDateOnly(startIso)
69-
const finalEnd = parseIsoDateOnly(endIso)
51+
let cursorStart = parseIsoDate(startIso)
52+
const finalEnd = parseIsoDate(endIso)
7053

7154
while (cursorStart.getTime() <= finalEnd.getTime()) {
7255
const cursorEnd = addDays(cursorStart, maximumDaysPerRequest - 1)
@@ -149,8 +132,8 @@ function buildWeeklyEvolutionFromContributorCounts(
149132

150133
const clampedWeekEndDate = weekEndDate.getTime() > rangeEnd.getTime() ? rangeEnd : weekEndDate
151134

152-
const weekStartIso = toIsoDateString(weekStartDate)
153-
const weekEndIso = toIsoDateString(clampedWeekEndDate)
135+
const weekStartIso = toIsoDate(weekStartDate)
136+
const weekEndIso = toIsoDate(clampedWeekEndDate)
154137

155138
return {
156139
value,
@@ -326,11 +309,11 @@ export function useCharts() {
326309
)
327310

328311
const endDateOnly = toDateOnly(evolutionOptions.endDate)
329-
const end = endDateOnly ? parseIsoDateOnly(endDateOnly) : yesterday
312+
const end = endDateOnly ? parseIsoDate(endDateOnly) : yesterday
330313

331314
const startDateOnly = toDateOnly(evolutionOptions.startDate)
332315
if (startDateOnly) {
333-
const start = parseIsoDateOnly(startDateOnly)
316+
const start = parseIsoDate(startDateOnly)
334317
return { start, end }
335318
}
336319

@@ -376,8 +359,8 @@ export function useCharts() {
376359

377360
const { start, end } = resolveDateRange(resolvedOptions, resolvedCreatedIso)
378361

379-
const startIso = toIsoDateString(start)
380-
const endIso = toIsoDateString(end)
362+
const startIso = toIsoDate(start)
363+
const endIso = toIsoDate(end)
381364

382365
const sortedDaily = await fetchDailyRangeChunked(resolvedPackageName, startIso, endIso)
383366

@@ -420,8 +403,8 @@ export function useCharts() {
420403
const sortedDaily = await dailyLikesPromise
421404

422405
const { start, end } = resolveDateRange(resolvedOptions, null)
423-
const startIso = toIsoDateString(start)
424-
const endIso = toIsoDateString(end)
406+
const startIso = toIsoDate(start)
407+
const endIso = toIsoDate(end)
425408

426409
const filteredDaily = sortedDaily.filter(d => d.day >= startIso && d.day <= endIso)
427410

app/utils/chart-data-buckets.ts

Lines changed: 16 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -5,34 +5,7 @@ import type {
55
WeeklyDataPoint,
66
YearlyDataPoint,
77
} from '~/types/chart'
8-
9-
// ---------------------------------------------------------------------------
10-
// Date helpers
11-
// ---------------------------------------------------------------------------
12-
13-
const DAY_MS = 86_400_000
14-
15-
function parseIso(value: string): Date {
16-
return new Date(`${value}T00:00:00.000Z`)
17-
}
18-
19-
function toIso(date: Date): string {
20-
return date.toISOString().slice(0, 10)
21-
}
22-
23-
function addDays(date: Date, days: number): Date {
24-
const d = new Date(date)
25-
d.setUTCDate(d.getUTCDate() + days)
26-
return d
27-
}
28-
29-
function daysInMonth(year: number, month: number): number {
30-
return new Date(Date.UTC(year, month + 1, 0)).getUTCDate()
31-
}
32-
33-
function daysInYear(year: number): number {
34-
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 366 : 365
35-
}
8+
import { DAY_MS, parseIsoDate, toIsoDate, addDays, daysInMonth, daysInYear } from '~/utils/date'
369

3710
// ---------------------------------------------------------------------------
3811
// Fill partial bucket
@@ -52,7 +25,11 @@ export function buildDailyEvolution(daily: DailyRawPoint[]): DailyDataPoint[] {
5225
return daily
5326
.slice()
5427
.sort((a, b) => a.day.localeCompare(b.day))
55-
.map(item => ({ day: item.day, value: item.value, timestamp: parseIso(item.day).getTime() }))
28+
.map(item => ({
29+
day: item.day,
30+
value: item.value,
31+
timestamp: parseIsoDate(item.day).getTime(),
32+
}))
5633
}
5734

5835
export function buildWeeklyEvolution(
@@ -63,18 +40,18 @@ export function buildWeeklyEvolution(
6340
const sorted = daily.slice().sort((a, b) => a.day.localeCompare(b.day))
6441
if (sorted.length === 0) return []
6542

66-
const rangeStartDate = parseIso(rangeStartIso)
43+
const rangeStartDate = parseIsoDate(rangeStartIso)
6744

6845
// Align from last day with actual data (npm has 1-2 day delay, today is incomplete)
6946
const lastNonZero = sorted.findLast(d => d.value > 0)
70-
const pickerEnd = parseIso(rangeEndIso)
71-
const effectiveEnd = lastNonZero ? parseIso(lastNonZero.day) : pickerEnd
47+
const pickerEnd = parseIsoDate(rangeEndIso)
48+
const effectiveEnd = lastNonZero ? parseIsoDate(lastNonZero.day) : pickerEnd
7249
const rangeEndDate = effectiveEnd.getTime() < pickerEnd.getTime() ? effectiveEnd : pickerEnd
7350

7451
// Group into 7-day buckets from END backwards
7552
const buckets = new Map<number, number>()
7653
for (const item of sorted) {
77-
const offset = Math.floor((rangeEndDate.getTime() - parseIso(item.day).getTime()) / DAY_MS)
54+
const offset = Math.floor((rangeEndDate.getTime() - parseIsoDate(item.day).getTime()) / DAY_MS)
7855
if (offset < 0) continue
7956
const idx = Math.floor(offset / 7)
8057
buckets.set(idx, (buckets.get(idx) ?? 0) + item.value)
@@ -94,8 +71,8 @@ export function buildWeeklyEvolution(
9471
value = fillPartialBucket(value, actualDays, 7)
9572
}
9673

97-
const weekStartIso = toIso(weekStartDate)
98-
const weekEndIso = toIso(weekEndDate)
74+
const weekStartIso = toIsoDate(weekStartDate)
75+
const weekEndIso = toIsoDate(weekEndDate)
9976
return {
10077
value,
10178
weekKey: `${weekStartIso}_${weekEndIso}`,
@@ -134,7 +111,7 @@ export function buildMonthlyEvolution(
134111
if (endDay < total) value = fillPartialBucket(value, endDay, total)
135112
}
136113

137-
return { month, value, timestamp: parseIso(`${month}-01`).getTime() }
114+
return { month, value, timestamp: parseIsoDate(`${month}-01`).getTime() }
138115
})
139116
}
140117

@@ -154,17 +131,17 @@ export function buildYearlyEvolution(
154131

155132
return entries.map(([year, value], i) => {
156133
const total = daysInYear(Number(year))
157-
const yearStart = parseIso(`${year}-01-01`)
134+
const yearStart = parseIsoDate(`${year}-01-01`)
158135

159136
if (i === 0 && rangeStartIso) {
160137
const dayOfYear = Math.floor(
161-
(parseIso(rangeStartIso).getTime() - yearStart.getTime()) / DAY_MS,
138+
(parseIsoDate(rangeStartIso).getTime() - yearStart.getTime()) / DAY_MS,
162139
)
163140
if (dayOfYear > 0) value = fillPartialBucket(value, total - dayOfYear, total)
164141
}
165142
if (i === entries.length - 1 && rangeEndIso) {
166143
const actualDays =
167-
Math.floor((parseIso(rangeEndIso).getTime() - yearStart.getTime()) / DAY_MS) + 1
144+
Math.floor((parseIsoDate(rangeEndIso).getTime() - yearStart.getTime()) / DAY_MS) + 1
168145
if (actualDays < total) value = fillPartialBucket(value, actualDays, total)
169146
}
170147

app/utils/date.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// TODO: when temporal?
2+
export const DAY_MS = 86_400_000
3+
4+
export function parseIsoDate(value: string): Date {
5+
return new Date(`${value}T00:00:00.000Z`)
6+
}
7+
8+
export function toIsoDate(date: Date): string {
9+
return date.toISOString().slice(0, 10)
10+
}
11+
12+
export function addDays(date: Date, days: number): Date {
13+
const d = new Date(date)
14+
d.setUTCDate(d.getUTCDate() + days)
15+
return d
16+
}
17+
18+
export function daysInMonth(year: number, month: number): number {
19+
return new Date(Date.UTC(year, month + 1, 0)).getUTCDate()
20+
}
21+
22+
export function daysInYear(year: number): number {
23+
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 366 : 365
24+
}

test/unit/app/utils/date.spec.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { describe, expect, it } from 'vitest'
2+
import {
3+
addDays,
4+
DAY_MS,
5+
daysInMonth,
6+
daysInYear,
7+
parseIsoDate,
8+
toIsoDate,
9+
} from '../../../../app/utils/date'
10+
11+
describe('DAY_MS', () => {
12+
it('equals 86 400 000', () => {
13+
expect(DAY_MS).toBe(86_400_000)
14+
})
15+
})
16+
17+
describe('parseIsoDate', () => {
18+
it('returns a UTC midnight date', () => {
19+
const d = parseIsoDate('2024-03-15')
20+
expect(d.toISOString()).toBe('2024-03-15T00:00:00.000Z')
21+
})
22+
23+
it('does not shift across timezones', () => {
24+
const d = parseIsoDate('2024-01-01')
25+
expect(d.getUTCHours()).toBe(0)
26+
expect(d.getUTCFullYear()).toBe(2024)
27+
})
28+
})
29+
30+
describe('toIsoDate', () => {
31+
it('formats a date as YYYY-MM-DD', () => {
32+
expect(toIsoDate(new Date('2024-03-15T00:00:00.000Z'))).toBe('2024-03-15')
33+
})
34+
35+
it('roundtrips with parseIsoDate', () => {
36+
const iso = '2024-12-31'
37+
expect(toIsoDate(parseIsoDate(iso))).toBe(iso)
38+
})
39+
})
40+
41+
describe('addDays', () => {
42+
it('adds positive days', () => {
43+
const d = parseIsoDate('2024-03-01')
44+
expect(toIsoDate(addDays(d, 5))).toBe('2024-03-06')
45+
})
46+
47+
it('subtracts with negative days', () => {
48+
const d = parseIsoDate('2024-03-10')
49+
expect(toIsoDate(addDays(d, -3))).toBe('2024-03-07')
50+
})
51+
52+
it('crosses month boundary', () => {
53+
const d = parseIsoDate('2024-01-30')
54+
expect(toIsoDate(addDays(d, 3))).toBe('2024-02-02')
55+
})
56+
57+
it('does not mutate the original date', () => {
58+
const d = parseIsoDate('2024-06-15')
59+
addDays(d, 10)
60+
expect(toIsoDate(d)).toBe('2024-06-15')
61+
})
62+
})
63+
64+
describe('daysInMonth', () => {
65+
it('returns 31 for January', () => {
66+
expect(daysInMonth(2024, 0)).toBe(31)
67+
})
68+
69+
it('returns 29 for Feb in a leap year', () => {
70+
expect(daysInMonth(2024, 1)).toBe(29)
71+
})
72+
73+
it('returns 28 for Feb in a non-leap year', () => {
74+
expect(daysInMonth(2023, 1)).toBe(28)
75+
})
76+
77+
it('returns 30 for April', () => {
78+
expect(daysInMonth(2024, 3)).toBe(30)
79+
})
80+
})
81+
82+
describe('daysInYear', () => {
83+
it('returns 366 for a leap year', () => {
84+
expect(daysInYear(2024)).toBe(366)
85+
})
86+
87+
it('returns 365 for a non-leap year', () => {
88+
expect(daysInYear(2023)).toBe(365)
89+
})
90+
91+
it('returns 365 for century non-leap year', () => {
92+
expect(daysInYear(1900)).toBe(365)
93+
})
94+
95+
it('returns 366 for year 2000 (divisible by 400)', () => {
96+
expect(daysInYear(2000)).toBe(366)
97+
})
98+
})

0 commit comments

Comments
 (0)