diff --git a/CHANGELOG.md b/CHANGELOG.md index fb6b3ef37..84f2882be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added the platform logo to the account selector in the create or update activity dialog +### Fixed + +- Fixed the date handling in the historical market data editor so entries no longer shift to the previous day for users in timezones behind UTC + ## 3.42.0 - 2026-08-04 ### Changed diff --git a/libs/common/src/lib/helper.spec.ts b/libs/common/src/lib/helper.spec.ts index 669c42e32..0a8637108 100644 --- a/libs/common/src/lib/helper.spec.ts +++ b/libs/common/src/lib/helper.spec.ts @@ -4,6 +4,7 @@ import { } from '@ghostfolio/common/config'; import { extractNumberFromString, + getLocalDateFromUtcDate, getNumberFormatGroup, getStringOrNull, getStringOrUndefined, @@ -355,4 +356,36 @@ describe('Helper', () => { ).toEqual(true); }); }); + + describe('Get local date from UTC date', () => { + const originalTz = process.env.TZ; + + afterEach(() => { + process.env.TZ = originalTz; + }); + + it('Preserve the calendar date in a timezone behind UTC', () => { + process.env.TZ = 'America/New_York'; + + const localDate = getLocalDateFromUtcDate( + new Date('2026-01-05T00:00:00.000Z') + ); + + expect(localDate.getFullYear()).toEqual(2026); + expect(localDate.getMonth()).toEqual(0); + expect(localDate.getDate()).toEqual(5); + }); + + it('Preserve the calendar date in a timezone ahead of UTC', () => { + process.env.TZ = 'Asia/Tokyo'; + + const localDate = getLocalDateFromUtcDate( + new Date('2026-01-05T00:00:00.000Z') + ); + + expect(localDate.getFullYear()).toEqual(2026); + expect(localDate.getMonth()).toEqual(0); + expect(localDate.getDate()).toEqual(5); + }); + }); }); diff --git a/libs/common/src/lib/helper.ts b/libs/common/src/lib/helper.ts index 6c0d6ea45..3819e5cee 100644 --- a/libs/common/src/lib/helper.ts +++ b/libs/common/src/lib/helper.ts @@ -359,6 +359,14 @@ export function getEmojiFlag(aCountryCode: string) { ); } +export function getLocalDateFromUtcDate(aUtcDate: Date) { + return new Date( + aUtcDate.getUTCFullYear(), + aUtcDate.getUTCMonth(), + aUtcDate.getUTCDate() + ); +} + export function getLocale() { return navigator.language ?? DEFAULT_LOCALE; } diff --git a/libs/ui/src/lib/services/data.service.ts b/libs/ui/src/lib/services/data.service.ts index 7081f9b2c..942f1b2b6 100644 --- a/libs/ui/src/lib/services/data.service.ts +++ b/libs/ui/src/lib/services/data.service.ts @@ -16,7 +16,10 @@ import { UpdateTagDto, UpdateUserSettingDto } from '@ghostfolio/common/dtos'; -import { DATE_FORMAT } from '@ghostfolio/common/helper'; +import { + DATE_FORMAT, + getLocalDateFromUtcDate +} from '@ghostfolio/common/helper'; import { Access, AccessTokenResponse, @@ -586,7 +589,7 @@ export class DataService { .pipe( map((data) => { for (const item of data.marketData) { - item.date = parseISO(item.date); + item.date = getLocalDateFromUtcDate(parseISO(item.date)); } for (const item of data.splits ?? []) {