mirror of https://github.com/ghostfolio/ghostfolio
7 changed files with 368 additions and 284 deletions
@ -1,50 +0,0 @@ |
|||||
import { |
|
||||
getIntradayChartItems, |
|
||||
INTRADAY_SAMPLE_INTERVAL_MS, |
|
||||
loadIntradaySamples, |
|
||||
recordIntradaySample |
|
||||
} from './intraday-samples'; |
|
||||
|
|
||||
describe('intraday samples', () => { |
|
||||
const now = new Date(2026, 9, 21, 10, 0, 0); |
|
||||
|
|
||||
beforeEach(() => { |
|
||||
localStorage.clear(); |
|
||||
}); |
|
||||
|
|
||||
it('records at most one sample per minute', () => { |
|
||||
recordIntradaySample({ now, value: 1 }); |
|
||||
recordIntradaySample({ now: new Date(now.getTime() + 5_000), value: 2 }); |
|
||||
|
|
||||
expect(loadIntradaySamples(now)).toEqual([ |
|
||||
{ timestamp: now.getTime(), value: 2 } |
|
||||
]); |
|
||||
|
|
||||
recordIntradaySample({ |
|
||||
now: new Date(now.getTime() + INTRADAY_SAMPLE_INTERVAL_MS), |
|
||||
value: 3 |
|
||||
}); |
|
||||
|
|
||||
expect(loadIntradaySamples(now)).toHaveLength(2); |
|
||||
}); |
|
||||
|
|
||||
it('removes the samples of previous days', () => { |
|
||||
recordIntradaySample({ now: new Date(2026, 9, 20, 15, 0), value: 1 }); |
|
||||
recordIntradaySample({ now, value: 2 }); |
|
||||
|
|
||||
expect(loadIntradaySamples(new Date(2026, 9, 20))).toEqual([]); |
|
||||
expect(loadIntradaySamples(now)).toHaveLength(1); |
|
||||
}); |
|
||||
|
|
||||
it('starts the chart of the day with the baseline at midnight', () => { |
|
||||
const items = getIntradayChartItems({ |
|
||||
now, |
|
||||
baseline: 0, |
|
||||
samples: [{ timestamp: now.getTime(), value: 0.5 }] |
|
||||
}); |
|
||||
|
|
||||
expect(items).toHaveLength(2); |
|
||||
expect(new Date(items[0].date).getHours()).toBe(0); |
|
||||
expect(items[1].value).toBe(0.5); |
|
||||
}); |
|
||||
}); |
|
||||
@ -1,83 +0,0 @@ |
|||||
import { LineChartItem } from '@ghostfolio/common/interfaces'; |
|
||||
|
|
||||
import { format, startOfDay } from 'date-fns'; |
|
||||
|
|
||||
export const INTRADAY_SAMPLES_KEY_PREFIX = 'gf-intraday-'; |
|
||||
export const INTRADAY_SAMPLE_INTERVAL_MS = 60_000; |
|
||||
|
|
||||
export interface IntradaySample { |
|
||||
timestamp: number; |
|
||||
value: number; |
|
||||
} |
|
||||
|
|
||||
function getKey(aDate: Date) { |
|
||||
return `${INTRADAY_SAMPLES_KEY_PREFIX}${format(aDate, 'yyyy-MM-dd')}`; |
|
||||
} |
|
||||
|
|
||||
export function loadIntradaySamples(aDate = new Date()): IntradaySample[] { |
|
||||
try { |
|
||||
const samples = JSON.parse(localStorage.getItem(getKey(aDate)) ?? '[]'); |
|
||||
|
|
||||
return Array.isArray(samples) ? samples : []; |
|
||||
} catch { |
|
||||
return []; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Stores the current value of the day (at most one sample per minute) in the |
|
||||
* local storage of the device, because only a single value per day is stored |
|
||||
* on the server. Samples of previous days are removed. |
|
||||
*/ |
|
||||
export function recordIntradaySample({ |
|
||||
now = new Date(), |
|
||||
value |
|
||||
}: { |
|
||||
now?: Date; |
|
||||
value: number; |
|
||||
}): IntradaySample[] { |
|
||||
const samples = loadIntradaySamples(now); |
|
||||
const last = samples.at(-1); |
|
||||
|
|
||||
if (!last || now.getTime() - last.timestamp >= INTRADAY_SAMPLE_INTERVAL_MS) { |
|
||||
samples.push({ value, timestamp: now.getTime() }); |
|
||||
} else { |
|
||||
// Keep the latest value of the current minute
|
|
||||
last.value = value; |
|
||||
} |
|
||||
|
|
||||
try { |
|
||||
for (let index = localStorage.length - 1; index >= 0; index--) { |
|
||||
const key = localStorage.key(index); |
|
||||
|
|
||||
if (key?.startsWith(INTRADAY_SAMPLES_KEY_PREFIX) && key !== getKey(now)) { |
|
||||
localStorage.removeItem(key); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
localStorage.setItem(getKey(now), JSON.stringify(samples)); |
|
||||
} catch {} |
|
||||
|
|
||||
return samples; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Builds the chart of the current day: the baseline (the close of the previous |
|
||||
* day) at the start of the day followed by the recorded samples of the day. |
|
||||
*/ |
|
||||
export function getIntradayChartItems({ |
|
||||
baseline, |
|
||||
now = new Date(), |
|
||||
samples |
|
||||
}: { |
|
||||
baseline: number; |
|
||||
now?: Date; |
|
||||
samples: IntradaySample[]; |
|
||||
}): LineChartItem[] { |
|
||||
return [ |
|
||||
{ date: startOfDay(now).toISOString(), value: baseline }, |
|
||||
...samples.map(({ timestamp, value }) => { |
|
||||
return { value, date: new Date(timestamp).toISOString() }; |
|
||||
}) |
|
||||
]; |
|
||||
} |
|
||||
Loading…
Reference in new issue