mirror of https://github.com/ghostfolio/ghostfolio
5 changed files with 188 additions and 18 deletions
@ -0,0 +1,50 @@ |
|||||
|
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); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,83 @@ |
|||||
|
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