Skip to content

Commit fb00533

Browse files
authored
fix: account for daylight savings time [LIBS-490] #1345
2 parents 3152c9f + 7911f8b commit fb00533

2 files changed

Lines changed: 101 additions & 24 deletions

File tree

services/config/src/__tests__/useTimeZoneConversion.test.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,24 @@ describe('useTimeZoneConversion', () => {
4545
expect(serverDate.getClientZonedISOString()).toBe(expectedDateString)
4646
})
4747

48+
it('returns fromServerDate that corrects for server time zone (adjusting for summer time)', () => {
49+
const systemInfo = {
50+
...defaultSystemInfo,
51+
serverTimeZoneId: 'Europe/Oslo',
52+
}
53+
const config = { ...defaultConfig, systemInfo }
54+
const wrapper = ({ children }: { children?: ReactNode }) => (
55+
<ConfigProvider config={config}>{children}</ConfigProvider>
56+
)
57+
const { result } = renderHook(() => useTimeZoneConversion(), {
58+
wrapper,
59+
})
60+
61+
const serverDate = result.current.fromServerDate('2010-07-01')
62+
const expectedDateString = '2010-06-30T22:00:00.000'
63+
expect(serverDate.getClientZonedISOString()).toBe(expectedDateString)
64+
})
65+
4866
// fromServerDate accepts number, valid date string, or date object
4967
it('returns fromServerDate which accepts number, valid date string, or date object', () => {
5068
const config = { ...defaultConfig, systemInfo: defaultSystemInfo }
@@ -107,6 +125,31 @@ describe('useTimeZoneConversion', () => {
107125
expect(serverDate.getClientZonedISOString()).toBe(expectedDateString)
108126
})
109127

128+
it('returns fromServerDate that assumes no time zone difference if client and server time zones are the same', () => {
129+
const systemInfo = {
130+
...defaultSystemInfo,
131+
serverTimeZoneId: 'Africa/Kampala',
132+
}
133+
jest.spyOn(Intl, 'DateTimeFormat').mockReturnValue({
134+
resolvedOptions: () => {
135+
return {
136+
timeZone: 'Africa/Kampala',
137+
}
138+
},
139+
} as Intl.DateTimeFormat)
140+
const config = { ...defaultConfig, systemInfo }
141+
const wrapper = ({ children }: { children?: ReactNode }) => (
142+
<ConfigProvider config={config}>{children}</ConfigProvider>
143+
)
144+
const { result } = renderHook(() => useTimeZoneConversion(), {
145+
wrapper,
146+
})
147+
148+
const serverDate = result.current.fromServerDate('2010-01-01')
149+
const expectedDateString = '2010-01-01T00:00:00.000'
150+
expect(serverDate.getClientZonedISOString()).toBe(expectedDateString)
151+
})
152+
110153
it('returns fromServerDate with server date that matches passed time regardless of timezone', () => {
111154
const systemInfo = {
112155
...defaultSystemInfo,

services/config/src/useTimeZoneConversion.ts

Lines changed: 58 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -50,40 +50,64 @@ class DHIS2Date extends Date {
5050
}
5151
}
5252

53-
const useServerTimeOffset = (serverTimezone: string): number => {
53+
const calculateOffset = (inputDate: any, serverTimezone: string) => {
54+
// we need to assume that the inputDate is in the client time zone due to limitations of javascript logic
55+
// note that this assumption is will be imperfect around daylight savings time changes
56+
const thenClientTime = new Date(inputDate)
57+
thenClientTime.setMilliseconds(0)
58+
59+
// 'sv' is used for localeString because it is the closest to ISO format
60+
// in principle, any locale should be parsable back to a date, but we encountered an error
61+
// when using en-US in certain environments, which we could not replicate when using 'sv'
62+
// Converting to localeString and then back to date is unfortunately the only current way
63+
// to construct a date that accounts for timezone.
64+
const serverLocaleString = thenClientTime.toLocaleString('sv', {
65+
timeZone: serverTimezone,
66+
})
67+
68+
const thenServerTimeZone = new Date(serverLocaleString)
69+
70+
return thenClientTime.getTime() - thenServerTimeZone.getTime()
71+
}
72+
73+
/**
74+
* Determines if the server/client time zone offset can and should be calculated
75+
* @param {string} serverTimezone string representation of server time zone (Area/Location)
76+
* * @param {string} clientTimezone string representation of client time zone (Area/Location)
77+
* @return {boolean} shouldCalculateOffset
78+
*/
79+
80+
const useShouldCalculateOffset = (
81+
serverTimezone: string,
82+
clientTimezone: string
83+
): boolean => {
5484
return useMemo(() => {
85+
// if client and server time zones are the same, offset is 0 and does not need to be subsequently calculated
86+
if (serverTimezone === clientTimezone) {
87+
return false
88+
}
89+
// attempt to calculate current time zone offset, if calcublable: return true; if not calculable, alert and return false
5590
try {
5691
const nowClientTime = new Date()
57-
nowClientTime.setMilliseconds(0)
58-
59-
// 'sv' is used for localeString because it is the closest to ISO format
60-
// in principle, any locale should be parsable back to a date, but we encountered an error
61-
// when using en-US in certain environments, which we could not replicate when using 'sv'
62-
// Converting to localeString and then back to date is unfortunately the only current way
63-
// to construct a date that accounts for timezone.
64-
const serverLocaleString = nowClientTime.toLocaleString('sv', {
65-
timeZone: serverTimezone,
66-
})
67-
const nowServerTimeZone = new Date(serverLocaleString)
68-
nowServerTimeZone.setMilliseconds(0)
69-
70-
return nowClientTime.getTime() - nowServerTimeZone.getTime()
92+
calculateOffset(nowClientTime, serverTimezone)
93+
return true
7194
} catch (err) {
7295
console.error(
7396
'Server time offset could not be determined; assuming no client/server difference',
7497
err
7598
)
7699
// if date is not constructable with timezone, assume 0 difference between client/server
77-
return 0
100+
return false
78101
}
79-
}, [serverTimezone])
102+
}, [serverTimezone, clientTimezone])
80103
}
81104

82105
export const useTimeZoneConversion = (): {
83106
fromServerDate: (date?: DateInput) => DHIS2Date
84107
fromClientDate: (date?: DateInput) => DHIS2Date
85108
} => {
86109
const { systemInfo } = useConfig()
110+
87111
let serverTimezone: string
88112
const clientTimezone: string =
89113
Intl.DateTimeFormat().resolvedOptions().timeZone
@@ -98,35 +122,45 @@ export const useTimeZoneConversion = (): {
98122
)
99123
}
100124

101-
const serverOffset = useServerTimeOffset(serverTimezone)
125+
const shouldCalculateOffset = useShouldCalculateOffset(
126+
serverTimezone,
127+
clientTimezone
128+
)
102129

103130
const fromServerDate = useCallback(
104131
(date) => {
105-
const serverDate = new Date(date)
132+
const jsServerDate = date ? new Date(date) : new Date(Date.now())
133+
const offset = shouldCalculateOffset
134+
? calculateOffset(jsServerDate, serverTimezone)
135+
: 0
106136
const clientDate = new DHIS2Date({
107-
date: serverDate.getTime() + serverOffset,
108-
serverOffset,
137+
date: jsServerDate.getTime() + offset,
138+
serverOffset: offset,
109139
serverTimezone,
110140
clientTimezone,
111141
})
112142

113143
return clientDate
114144
},
115-
[serverOffset, serverTimezone, clientTimezone]
145+
[shouldCalculateOffset, serverTimezone, clientTimezone]
116146
)
117147

118148
const fromClientDate = useCallback(
119149
(date) => {
150+
const jsClientDate = date ? new Date(date) : new Date(Date.now())
151+
const offset = shouldCalculateOffset
152+
? calculateOffset(jsClientDate, serverTimezone)
153+
: 0
120154
const clientDate = new DHIS2Date({
121155
date,
122-
serverOffset,
156+
serverOffset: offset,
123157
serverTimezone,
124158
clientTimezone,
125159
})
126160

127161
return clientDate
128162
},
129-
[serverOffset, serverTimezone, clientTimezone]
163+
[shouldCalculateOffset, serverTimezone, clientTimezone]
130164
)
131165

132166
return useMemo(

0 commit comments

Comments
 (0)