Skip to content

Commit a6c9578

Browse files
committed
adding tests
1 parent b6a33ae commit a6c9578

1 file changed

Lines changed: 180 additions & 0 deletions

File tree

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import { describe, expect, it, vi } from 'vitest'
2+
3+
vi.mock('~/utils/chart-data-correction', () => ({
4+
applyDataCorrection: (data: { value: number }[]) => data,
5+
}))
6+
7+
import {
8+
endDateOnlyToUtcMs,
9+
getBucketStartUtc,
10+
getBucketEndUtc,
11+
getCompletionRatio,
12+
linearProject,
13+
extrapolateLastValue,
14+
} from '../../../../app/utils/chart-data-prediction'
15+
16+
describe('endDateOnlyToUtcMs', () => {
17+
it('parses a valid date', () => {
18+
expect(endDateOnlyToUtcMs('2025-03-15')).toBe(Date.UTC(2025, 2, 15, 23, 59, 59, 999))
19+
})
20+
21+
it('returns null for invalid formats', () => {
22+
expect(endDateOnlyToUtcMs('not-a-date')).toBeNull()
23+
expect(endDateOnlyToUtcMs('2025/03/15')).toBeNull()
24+
expect(endDateOnlyToUtcMs('')).toBeNull()
25+
})
26+
})
27+
28+
describe('getBucketStartUtc', () => {
29+
// Friday 2025-11-07 at 06:05:04
30+
const ts = Date.UTC(2025, 10, 7, 6, 5, 4)
31+
32+
it('yearly → Jan 1st', () => {
33+
expect(getBucketStartUtc(ts, 'yearly')).toBe(Date.UTC(2025, 0, 1))
34+
})
35+
36+
it('monthly → 1st of month', () => {
37+
expect(getBucketStartUtc(ts, 'monthly')).toBe(Date.UTC(2025, 10, 1))
38+
})
39+
40+
it('weekly → previous Monday', () => {
41+
expect(getBucketStartUtc(ts, 'weekly')).toBe(Date.UTC(2025, 10, 3))
42+
})
43+
44+
it('weekly on a Monday returns that Monday', () => {
45+
const monday = Date.UTC(2025, 10, 3, 6, 5, 4)
46+
expect(getBucketStartUtc(monday, 'weekly')).toBe(Date.UTC(2025, 10, 3))
47+
})
48+
49+
it('weekly on a Sunday returns previous Monday', () => {
50+
const sunday = Date.UTC(2025, 10, 9, 6, 5, 4)
51+
expect(getBucketStartUtc(sunday, 'weekly')).toBe(Date.UTC(2025, 10, 3))
52+
})
53+
54+
it('daily → start of day', () => {
55+
expect(getBucketStartUtc(ts, 'daily')).toBe(Date.UTC(2025, 10, 7))
56+
})
57+
})
58+
59+
describe('getBucketEndUtc', () => {
60+
// Friday 2025-11-07 at 06:05:04
61+
const ts = Date.UTC(2025, 10, 7, 6, 5, 4)
62+
63+
it('yearly → Jan 1st next year', () => {
64+
expect(getBucketEndUtc(ts, 'yearly')).toBe(Date.UTC(2026, 0, 1))
65+
})
66+
67+
it('monthly → 1st of next month', () => {
68+
expect(getBucketEndUtc(ts, 'monthly')).toBe(Date.UTC(2025, 11, 1))
69+
})
70+
71+
it('weekly → Monday + 7 days', () => {
72+
expect(getBucketEndUtc(ts, 'weekly')).toBe(Date.UTC(2025, 10, 10))
73+
})
74+
75+
it('daily → next day', () => {
76+
expect(getBucketEndUtc(ts, 'daily')).toBe(Date.UTC(2025, 10, 8))
77+
})
78+
})
79+
80+
describe('getCompletionRatio', () => {
81+
it('returns 0 at bucket start', () => {
82+
const start = Date.UTC(2025, 2, 1)
83+
expect(getCompletionRatio(start, 'monthly', start)).toBe(0)
84+
})
85+
86+
it('returns 1 at bucket end', () => {
87+
const start = Date.UTC(2025, 2, 1)
88+
const end = Date.UTC(2025, 3, 1)
89+
expect(getCompletionRatio(start, 'monthly', end)).toBe(1)
90+
})
91+
92+
it('returns ~0.5 at midpoint', () => {
93+
const start = Date.UTC(2025, 0, 1)
94+
const mid = Date.UTC(2025, 0, 1, 12, 0)
95+
expect(getCompletionRatio(start, 'daily', mid)).toBeCloseTo(0.5)
96+
})
97+
98+
it('clamps to [0, 1]', () => {
99+
const start = Date.UTC(2025, 2, 1)
100+
expect(getCompletionRatio(start, 'monthly', start - 1000)).toBe(0)
101+
expect(getCompletionRatio(start, 'monthly', Date.UTC(2025, 5, 1))).toBe(1)
102+
})
103+
})
104+
105+
describe('linearProject', () => {
106+
it('returns null with fewer than 2 points', () => {
107+
expect(linearProject([])).toBeNull()
108+
expect(linearProject([5])).toBeNull()
109+
})
110+
111+
it('projects a linear trend', () => {
112+
expect(linearProject([10, 20, 30])).toBeCloseTo(40)
113+
})
114+
115+
it('floors negative projections to 0', () => {
116+
expect(linearProject([100, 10, 1])).toBe(0)
117+
})
118+
119+
it('handles constant series', () => {
120+
expect(linearProject([5, 5, 5])).toBeCloseTo(5)
121+
})
122+
})
123+
124+
describe('extrapolateLastValue', () => {
125+
it('returns raw last value when ratio >= 1 (complete bucket)', () => {
126+
const lastDateMs = Date.UTC(2025, 1, 1)
127+
const result = extrapolateLastValue({
128+
series: [100, 200, 150],
129+
granularity: 'monthly',
130+
lastDateMs,
131+
referenceMs: Date.UTC(2025, 2, 1), // bucket fully elapsed
132+
predictionPoints: 4,
133+
})
134+
expect(result).toBe(150)
135+
})
136+
137+
it('returns raw last value when predictionPoints <= 0', () => {
138+
const result = extrapolateLastValue({
139+
series: [100, 200, 50],
140+
granularity: 'monthly',
141+
lastDateMs: Date.UTC(2025, 2, 1),
142+
referenceMs: Date.UTC(2025, 2, 15),
143+
predictionPoints: 0,
144+
})
145+
expect(result).toBe(50)
146+
})
147+
148+
it('uses linear projection when enough lookback points', () => {
149+
const result = extrapolateLastValue({
150+
series: [100, 200, 300, 50],
151+
granularity: 'monthly',
152+
lastDateMs: Date.UTC(2025, 2, 1),
153+
referenceMs: Date.UTC(2025, 2, 15),
154+
predictionPoints: 4,
155+
})
156+
expect(result).toBeCloseTo(400)
157+
})
158+
159+
it('copies single lookback point when only one available', () => {
160+
const result = extrapolateLastValue({
161+
series: [100, 10],
162+
granularity: 'monthly',
163+
lastDateMs: Date.UTC(2025, 2, 1),
164+
referenceMs: Date.UTC(2025, 2, 15),
165+
predictionPoints: 4,
166+
})
167+
expect(result).toBe(100)
168+
})
169+
170+
it('falls back to proportional scale-up with no lookback', () => {
171+
const result = extrapolateLastValue({
172+
series: [50],
173+
granularity: 'daily',
174+
lastDateMs: Date.UTC(2025, 2, 12),
175+
referenceMs: Date.UTC(2025, 2, 12, 12, 0), // half day
176+
predictionPoints: 4,
177+
})
178+
expect(result).toBeCloseTo(100)
179+
})
180+
})

0 commit comments

Comments
 (0)