`@progress/kendo-intl` incorrectly formats timezone offsets whose minute component is non-zero.
For example, a JavaScript `Date` with the historical Warsaw offset `UTC+01:24` is formatted as `+01:04` instead of `+01:24`.
This affects all tested `Z`, `X`, and `x` timezone format variants.
- `@progress/kendo-angular-intl`: `24.2.0`
- `@progress/kendo-intl`: `3.2.1`
- Browser/Node timezone: `Europe/Warsaw`
- Operating system: Windows
- Format: `yyyy-MM-ddTHH:mm:ssZZZZZ`
The same formatting implementation appears in `@progress/kendo-intl` versions `3.1.2` and `3.2.1`.
const { formatDate } = require('@progress/kendo-intl');
process.env.TZ = 'Europe/Warsaw';
const date = new Date(1900, 0, 1, 16, 0, 0);
console.log(date.toString());
console.log(date.getTimezoneOffset());
console.log(formatDate(date, 'yyyy-MM-ddTHH:mm:ssZZZZZ'));Mon Jan 01 1900 16:00:00 GMT+0124
-84
1900-01-01T16:00:00+01:041900-01-01T16:00:00+01:24Date#getTimezoneOffset() returns -84, meaning the local timezone is 84 minutes ahead of UTC:
84 minutes = 1 hour 24 minutesThe issue is not limited to positive offsets:
getTimezoneOffset() | Expected | Actual |
|---|---|---|
-84 | +01:24 | +01:04 |
210 | -03:30 | -03:05 |
UTC is also formatted differently depending on the token, but that behavior may be intentional:
| Offset | Expected ISO representation | ZZZZZ result |
|---|---|---|
0 | +00:00 or Z | Z |
All applicable format variants use the same incorrect minute calculation:
Z -> +0104
ZZ -> +0104
ZZZ -> +0104
ZZZZ -> GMT+01:04
ZZZZZ -> +01:04
X -> +0104
XX -> +0104
XXX -> +01:04
XXXX -> +0104
XXXXX -> +01:04
x -> +0104
xxx -> +01:04
xxxxx -> +01:04Changing from ZZZZZ to XXXXX or XXX therefore does not resolve the problem.
The current formatter appears to perform approximately this calculation:
const offset = date.getTimezoneOffset() / 60;
const hoursMinutes = Math.abs(offset).toString().split('.');
const minutes = hoursMinutes[1] || 0;For an 84-minute offset:
84 / 60 = 1.4The decimal portion "4" is then treated as four minutes and padded to "04".
However, the fractional part represents a fraction of an hour:
0.4 hours * 60 = 24 minutesLikewise, for 210 minutes:
210 / 60 = 3.5The formatter produces 03:05, although 0.5 hours is 30 minutes.
Timezone offsets should remain integer minute values throughout formatting:
function formatTimeZone(date, info, options) {
const totalMinutes = date.getTimezoneOffset();
const absoluteMinutes = Math.abs(totalMinutes);
const hours = Math.floor(absoluteMinutes / 60);
const minutes = absoluteMinutes % 60;
const sign = totalMinutes <= 0 ? '+' : '-';
// Continue applying shortHours, separator, optionalMinutes,
// localizedName and zZeroOffset options using hours and minutes.
}The important calculations are:
const hours = Math.floor(Math.abs(offsetMinutes) / 60);
const minutes = Math.abs(offsetMinutes) % 60;Converting the offset to a decimal hour and splitting its string representation is not reliable.
it('formats a positive timezone offset with minutes', () => {
const date = new Date(2024, 5, 25, 12, 7, 5);
vi.spyOn(date, 'getTimezoneOffset').mockReturnValue(-84);
expect(formatDate(date, 'yyyy-MM-ddTHH:mm:ssZZZZZ'))
.toBe('2024-06-25T12:07:05+01:24');
});
it('formats a negative timezone offset with minutes', () => {
const date = new Date(2024, 5, 25, 12, 7, 5);
vi.spyOn(date, 'getTimezoneOffset').mockReturnValue(210);
expect(formatDate(date, 'yyyy-MM-ddTHH:mm:ssZZZZZ'))
.toBe('2024-06-25T12:07:05-03:30');
});Other useful cases include:
-345 -> +05:45
-330 -> +05:30
210 -> -03:30
0 -> Z or +00:00, depending on the selected token
This can corrupt serialized date-time values when they are passed to systems that honor the emitted offset, including .NET DateTimeOffset.
Example:
Intended: 1900-01-01T16:00:00+01:24
Emitted: 1900-01-01T16:00:00+01:04These values represent instants 20 minutes apart.
The issue affects:
Date implementation returning a non-whole-hour offset.Examples of modern fractional offsets include UTC+05:30, UTC+05:45, UTC+09:30, and UTC-03:30.
We initially encountered this with Europe/Warsaw and 1900-01-01. The JavaScript runtime correctly reports Warsaw's historical offset as UTC+01:24. The incorrect UTC+01:04 value is introduced only during Kendo formatting.
We have implemented a temporary application-level workaround that formats the date portion with Kendo and calculates the timezone offset directly from integer minutes.