Skip to content

Commit 334e3ca

Browse files
authored
Merge pull request #13 from sleeyax/feat/dst-transitions
2 parents c7348d7 + a7ccbb2 commit 334e3ca

File tree

599 files changed

+3805
-3704
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

599 files changed

+3805
-3704
lines changed

CHANGELOG.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,42 @@
11
# Changelog
22

3+
## 2025-10-01
4+
5+
### [Show DST Transitions When DST is Inactive](https://github.com/sleeyax/world-time-api/pull/13)
6+
7+
**When DST is NOT active** (`dst = false`):
8+
9+
- `dst_from` now shows when DST will begin next (was `null`)
10+
- `dst_until` now shows when DST last ended (was `null`)
11+
12+
**When DST is active** (`dst = true`):
13+
14+
- No change - same behavior as before
15+
16+
#### Example
17+
18+
Australia/Sydney on October 1, 2025 (DST not active):
19+
20+
```json
21+
{
22+
"dst": false,
23+
"dst_from": "2025-10-04T16:00:00+00:00", // DST starts Oct 5 at 2am local
24+
"dst_until": "2025-04-05T16:00:00+00:00" // DST ended Apr 6 at 3am local
25+
}
26+
```
27+
28+
#### Important: Times are in UTC
29+
30+
DST transition times are returned in UTC, not local time. For example:
31+
32+
- `2025-10-04T16:00:00+00:00` = October 5th at 2:00 AM Sydney time
33+
34+
This follows the API convention of using UTC for all absolute timestamps.
35+
36+
#### Breaking Change
37+
38+
Clients expecting `null` when DST is inactive will now receive date strings instead.
39+
340
## 2025-09-12
441

542
Initial release.

public/openapi.yaml

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
openapi: "3.0.1"
22
info:
33
title: "World Time API"
4-
version: "20250912"
4+
version: "2025-10-01"
55
description: >-
66
API to get the current local time details for a given timezone or IP address
77
@@ -510,6 +510,42 @@ components:
510510
utc_offset: "+10:00"
511511
week_number: 37
512512
client_ip: "127.0.0.1"
513+
dst_not_active:
514+
summary: "DST not active - shows when DST will begin (future) and last ended (past)"
515+
value:
516+
abbreviation: "AEST"
517+
datetime: "2025-10-01T10:58:52.533+10:00"
518+
day_of_week: 3
519+
day_of_year: 274
520+
dst: false
521+
dst_from: "2025-10-04T16:00:00+00:00"
522+
dst_offset: 0
523+
dst_until: "2025-04-05T16:00:00+00:00"
524+
raw_offset: 36000
525+
timezone: "Australia/Sydney"
526+
unixtime: 1759280332
527+
utc_datetime: "2025-10-01T00:58:52.533+00:00"
528+
utc_offset: "+10:00"
529+
week_number: 40
530+
client_ip: "127.0.0.1"
531+
dst_active:
532+
summary: "DST active - shows when DST started (past) and will end (future)"
533+
value:
534+
abbreviation: "CDT"
535+
datetime: "2025-07-31T11:05:04.562-05:00"
536+
day_of_week: 4
537+
day_of_year: 212
538+
dst: true
539+
dst_from: "2025-03-09T08:00:00+00:00"
540+
dst_offset: 3600
541+
dst_until: "2025-11-02T07:00:00+00:00"
542+
raw_offset: -21600
543+
timezone: "America/Chicago"
544+
unixtime: 1753977904
545+
utc_datetime: "2025-07-31T16:05:04.562+00:00"
546+
utc_offset: "-05:00"
547+
week_number: 31
548+
client_ip: "127.0.0.1"
513549
SuccessfulDateTimeTextResponse:
514550
description: >-
515551
the current time for the timezone requested in plain text
@@ -662,21 +698,25 @@ components:
662698
time is in daylight savings
663699
dst_from:
664700
type: string
701+
nullable: true
665702
description: >-
666-
an ISO8601-valid string representing
667-
the datetime when daylight savings
668-
started for this timezone
703+
an ISO8601-valid string representing the datetime of a DST transition.
704+
When DST is active (dst=true): shows when the current DST period started.
705+
When DST is NOT active (dst=false): shows when the next DST period will begin (future date).
706+
Returns null if the timezone does not observe DST.
669707
dst_offset:
670708
type: integer
671709
description: >-
672710
the difference in seconds between the current local
673711
time and daylight saving time for the location
674712
dst_until:
675713
type: string
714+
nullable: true
676715
description: >-
677-
an ISO8601-valid string representing
678-
the datetime when daylight savings
679-
will end for this timezone
716+
an ISO8601-valid string representing the datetime of a DST transition.
717+
When DST is active (dst=true): shows when the current DST period will end (future date).
718+
When DST is NOT active (dst=false): shows when the last DST period ended (past date).
719+
Returns null if the timezone does not observe DST.
680720
raw_offset:
681721
type: integer
682722
description: >-

src/services/timezone.ts

Lines changed: 48 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,8 @@ export function getTime(
3333
const dstOffset = normalizeZero(
3434
dst ? utcOffset.seconds() - utcOffsetRaw.seconds() : 0,
3535
);
36-
const dstTransitions = dst
37-
? getDstTransitions(timezone.name(), dateTime.year())
38-
: { dstStart: null, dstEnd: null };
36+
const dstTransitions = getDstTransitions(timezone.name(), dateTime, dst);
37+
3938
const dstFrom = dstTransitions.dstStart
4039
? toISOWithoutFractionalZeros(dstTransitions.dstStart.toIsoString())
4140
: null;
@@ -62,38 +61,63 @@ export function getTime(
6261
};
6362
}
6463

64+
/**
65+
* Get DST transition dates based on current time and DST status
66+
*
67+
* When DST is active:
68+
* - dstStart: when current DST period started
69+
* - dstEnd: when current DST period will end
70+
*
71+
* When DST is NOT active:
72+
* - dstStart: when next DST period will begin (future)
73+
* - dstEnd: when last DST period ended (past)
74+
*/
6575
function getDstTransitions(
6676
zoneName: string,
67-
year: number,
77+
currentTime: tc.DateTime,
78+
isDstActive: boolean,
6879
): { dstStart: tc.DateTime | null; dstEnd: tc.DateTime | null } {
6980
const db = tc.TzDatabase.instance();
7081

7182
if (!db.hasDst(zoneName)) {
7283
return { dstStart: null, dstEnd: null };
7384
}
7485

75-
// Start from beginning of year
76-
const startOfYear = new tc.DateTime(
77-
year,
78-
1,
79-
1,
80-
0,
81-
0,
82-
0,
83-
0,
84-
tc.utc(),
85-
).unixUtcMillis();
86-
87-
// Find next two DST changes from start of year
88-
const firstChange = db.nextDstChange(zoneName, startOfYear);
89-
const secondChange = firstChange
90-
? db.nextDstChange(zoneName, firstChange)
86+
const currentMillis = currentTime.unixUtcMillis();
87+
88+
// Find the next DST transition from current time.
89+
const nextChange = db.nextDstChange(zoneName, currentMillis);
90+
91+
// Find the previous DST transition by searching from one year ago.
92+
const oneYearAgo = currentTime.add(-1, tc.TimeUnit.Year).unixUtcMillis();
93+
let prevChange = db.nextDstChange(zoneName, oneYearAgo);
94+
95+
// Keep looking forward until we find the transition just before current time.
96+
while (prevChange && prevChange < currentMillis) {
97+
const nextTransition = db.nextDstChange(zoneName, prevChange);
98+
if (nextTransition && nextTransition < currentMillis) {
99+
prevChange = nextTransition;
100+
} else {
101+
break;
102+
}
103+
}
104+
105+
// Convert to DateTime objects.
106+
const prevDateTime = prevChange
107+
? new tc.DateTime(prevChange, tc.utc())
108+
: null;
109+
const nextDateTime = nextChange
110+
? new tc.DateTime(nextChange, tc.utc())
91111
: null;
92112

93-
return {
94-
dstStart: firstChange ? new tc.DateTime(firstChange, tc.utc()) : null,
95-
dstEnd: secondChange ? new tc.DateTime(secondChange, tc.utc()) : null,
96-
};
113+
// Return based on DST status.
114+
if (isDstActive) {
115+
// DST is active: prev = when it started, next = when it will end.
116+
return { dstStart: prevDateTime, dstEnd: nextDateTime };
117+
} else {
118+
// DST is NOT active: prev = when it last ended, next = when it will begin.
119+
return { dstStart: nextDateTime, dstEnd: prevDateTime };
120+
}
97121
}
98122

99123
function getZoneAbbreviation(dateTime: tc.DateTime, utcOffset: tc.Duration) {

tests/data/timezones/Africa_Abidjan.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
{
22
"utc_offset": "+00:00",
33
"timezone": "Africa/Abidjan",
4-
"day_of_week": 4,
5-
"day_of_year": 212,
6-
"datetime": "2025-07-31T16:02:37.389552+00:00",
7-
"utc_datetime": "2025-07-31T16:02:37.389552+00:00",
8-
"unixtime": 1753977757,
4+
"day_of_week": 3,
5+
"day_of_year": 274,
6+
"datetime": "2025-10-01T10:37:41.709530+00:00",
7+
"utc_datetime": "2025-10-01T10:37:41.709530+00:00",
8+
"unixtime": 1759315061,
99
"raw_offset": 0,
10-
"week_number": 31,
10+
"week_number": 40,
1111
"dst": false,
1212
"abbreviation": "GMT",
1313
"dst_offset": 0,

tests/data/timezones/Africa_Accra.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
{
22
"utc_offset": "+00:00",
33
"timezone": "Africa/Accra",
4-
"day_of_week": 4,
5-
"day_of_year": 212,
6-
"datetime": "2025-07-31T16:02:37.703223+00:00",
7-
"utc_datetime": "2025-07-31T16:02:37.703223+00:00",
8-
"unixtime": 1753977757,
4+
"day_of_week": 3,
5+
"day_of_year": 274,
6+
"datetime": "2025-10-01T10:37:42.021744+00:00",
7+
"utc_datetime": "2025-10-01T10:37:42.021744+00:00",
8+
"unixtime": 1759315062,
99
"raw_offset": 0,
10-
"week_number": 31,
10+
"week_number": 40,
1111
"dst": false,
1212
"abbreviation": "GMT",
1313
"dst_offset": 0,

tests/data/timezones/Africa_Addis_Ababa.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
{
22
"utc_offset": "+03:00",
33
"timezone": "Africa/Addis_Ababa",
4-
"day_of_week": 4,
5-
"day_of_year": 212,
6-
"datetime": "2025-07-31T19:02:38.041715+03:00",
7-
"utc_datetime": "2025-07-31T16:02:38.041715+00:00",
8-
"unixtime": 1753977758,
4+
"day_of_week": 3,
5+
"day_of_year": 274,
6+
"datetime": "2025-10-01T13:37:42.333598+03:00",
7+
"utc_datetime": "2025-10-01T10:37:42.333598+00:00",
8+
"unixtime": 1759315062,
99
"raw_offset": 10800,
10-
"week_number": 31,
10+
"week_number": 40,
1111
"dst": false,
1212
"abbreviation": "EAT",
1313
"dst_offset": 0,

tests/data/timezones/Africa_Algiers.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
{
22
"utc_offset": "+01:00",
33
"timezone": "Africa/Algiers",
4-
"day_of_week": 4,
5-
"day_of_year": 212,
6-
"datetime": "2025-07-31T17:02:39.293918+01:00",
7-
"utc_datetime": "2025-07-31T16:02:39.293918+00:00",
8-
"unixtime": 1753977759,
4+
"day_of_week": 3,
5+
"day_of_year": 274,
6+
"datetime": "2025-10-01T11:37:42.645592+01:00",
7+
"utc_datetime": "2025-10-01T10:37:42.645592+00:00",
8+
"unixtime": 1759315062,
99
"raw_offset": 3600,
10-
"week_number": 31,
10+
"week_number": 40,
1111
"dst": false,
1212
"abbreviation": "CET",
1313
"dst_offset": 0,

tests/data/timezones/Africa_Asmara.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
{
22
"utc_offset": "+03:00",
33
"timezone": "Africa/Asmara",
4-
"day_of_week": 4,
5-
"day_of_year": 212,
6-
"datetime": "2025-07-31T19:02:39.784317+03:00",
7-
"utc_datetime": "2025-07-31T16:02:39.784317+00:00",
8-
"unixtime": 1753977759,
4+
"day_of_week": 3,
5+
"day_of_year": 274,
6+
"datetime": "2025-10-01T13:37:42.943771+03:00",
7+
"utc_datetime": "2025-10-01T10:37:42.943771+00:00",
8+
"unixtime": 1759315062,
99
"raw_offset": 10800,
10-
"week_number": 31,
10+
"week_number": 40,
1111
"dst": false,
1212
"abbreviation": "EAT",
1313
"dst_offset": 0,

tests/data/timezones/Africa_Asmera.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
{
22
"utc_offset": "+03:00",
33
"timezone": "Africa/Asmera",
4-
"day_of_week": 4,
5-
"day_of_year": 212,
6-
"datetime": "2025-07-31T19:04:06.061282+03:00",
7-
"utc_datetime": "2025-07-31T16:04:06.061282+00:00",
8-
"unixtime": 1753977846,
4+
"day_of_week": 3,
5+
"day_of_year": 274,
6+
"datetime": "2025-10-01T13:37:43.243224+03:00",
7+
"utc_datetime": "2025-10-01T10:37:43.243224+00:00",
8+
"unixtime": 1759315063,
99
"raw_offset": 10800,
10-
"week_number": 31,
10+
"week_number": 40,
1111
"dst": false,
1212
"abbreviation": "EAT",
1313
"dst_offset": 0,

tests/data/timezones/Africa_Bamako.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
{
22
"utc_offset": "+00:00",
33
"timezone": "Africa/Bamako",
4-
"day_of_week": 4,
5-
"day_of_year": 212,
6-
"datetime": "2025-07-31T16:04:06.593001+00:00",
7-
"utc_datetime": "2025-07-31T16:04:06.593001+00:00",
8-
"unixtime": 1753977846,
4+
"day_of_week": 3,
5+
"day_of_year": 274,
6+
"datetime": "2025-10-01T10:37:43.543946+00:00",
7+
"utc_datetime": "2025-10-01T10:37:43.543946+00:00",
8+
"unixtime": 1759315063,
99
"raw_offset": 0,
10-
"week_number": 31,
10+
"week_number": 40,
1111
"dst": false,
1212
"abbreviation": "GMT",
1313
"dst_offset": 0,

0 commit comments

Comments
 (0)