@@ -123,12 +123,11 @@ describe('linearProject', () => {
123123
124124describe ( 'extrapolateLastValue' , ( ) => {
125125 it ( 'returns raw last value when ratio >= 1 (complete bucket)' , ( ) => {
126- const lastDateMs = Date . UTC ( 2025 , 1 , 1 )
127126 const result = extrapolateLastValue ( {
128127 series : [ 100 , 200 , 150 ] ,
129128 granularity : 'monthly' ,
130- lastDateMs,
131- referenceMs : Date . UTC ( 2025 , 2 , 1 ) , // bucket fully elapsed
129+ lastDateMs : Date . UTC ( 2025 , 1 , 1 ) ,
130+ referenceMs : Date . UTC ( 2025 , 2 , 1 ) ,
132131 predictionPoints : 4 ,
133132 } )
134133 expect ( result ) . toBe ( 150 )
@@ -145,36 +144,53 @@ describe('extrapolateLastValue', () => {
145144 expect ( result ) . toBe ( 50 )
146145 } )
147146
148- it ( 'uses linear projection when enough lookback points' , ( ) => {
147+ it ( 'uses linear projection when lookback >= predictionPoints' , ( ) => {
148+ // 4 lookback points, predictionPoints = 4 → linear projection
149149 const result = extrapolateLastValue ( {
150- series : [ 100 , 200 , 300 , 50 ] ,
150+ series : [ 100 , 200 , 300 , 400 , 50 ] ,
151151 granularity : 'monthly' ,
152- lastDateMs : Date . UTC ( 2025 , 2 , 1 ) ,
153- referenceMs : Date . UTC ( 2025 , 2 , 15 ) ,
152+ lastDateMs : Date . UTC ( 2025 , 3 , 1 ) ,
153+ referenceMs : Date . UTC ( 2025 , 3 , 15 ) ,
154154 predictionPoints : 4 ,
155155 } )
156- expect ( result ) . toBeCloseTo ( 400 )
156+ expect ( result ) . toBeCloseTo ( 500 )
157157 } )
158158
159- it ( 'copies single lookback point when only one available' , ( ) => {
159+ it ( 'falls back to scale-up when lookback < predictionPoints' , ( ) => {
160+ // 2 lookback points but predictionPoints = 4 → scale-up
160161 const result = extrapolateLastValue ( {
161- series : [ 100 , 10 ] ,
162+ series : [ 100 , 200 , 50 ] ,
162163 granularity : 'monthly' ,
163164 lastDateMs : Date . UTC ( 2025 , 2 , 1 ) ,
164- referenceMs : Date . UTC ( 2025 , 2 , 15 ) ,
165+ referenceMs : Date . UTC ( 2025 , 2 , 16 ) ,
165166 predictionPoints : 4 ,
166167 } )
167- expect ( result ) . toBe ( 100 )
168+ // ratio ≈ 15/31, scaled ≈ 50 / (15/31) ≈ 103.3
169+ expect ( result ) . toBeGreaterThan ( 50 )
170+ expect ( result ) . toBeLessThan ( 200 )
168171 } )
169172
170- it ( 'falls back to proportional scale-up with no lookback' , ( ) => {
173+ it ( 'falls back to scale-up with no lookback' , ( ) => {
171174 const result = extrapolateLastValue ( {
172175 series : [ 50 ] ,
173176 granularity : 'daily' ,
174177 lastDateMs : Date . UTC ( 2025 , 2 , 12 ) ,
175- referenceMs : Date . UTC ( 2025 , 2 , 12 , 12 , 0 ) , // half day
178+ referenceMs : Date . UTC ( 2025 , 2 , 12 , 12 , 0 ) ,
176179 predictionPoints : 4 ,
177180 } )
178181 expect ( result ) . toBeCloseTo ( 100 )
179182 } )
183+
184+ it ( 'yearly with few points uses scale-up' , ( ) => {
185+ // 2 complete years + partial 2025, predictionPoints = 4 → scale-up
186+ const result = extrapolateLastValue ( {
187+ series : [ 1000 , 2000 , 500 ] ,
188+ granularity : 'yearly' ,
189+ lastDateMs : Date . UTC ( 2025 , 0 , 1 ) ,
190+ referenceMs : Date . UTC ( 2025 , 2 , 10 ) ,
191+ predictionPoints : 4 ,
192+ } )
193+ // ~69 days into the year, ratio ≈ 0.19 → scaled ≈ 500 / 0.19 ≈ 2632
194+ expect ( result ) . toBeGreaterThan ( 2000 )
195+ } )
180196} )
0 commit comments