Skip to content

Commit 15a7d59

Browse files
dkhalanskyjbilya-g
authored andcommitted
Native: implement (untested) atStartOfDay
Tests fail on Windows because its record for Asia/Gaza is incomplete or inaccurate (it stores 23:59:59 as the start of the transition to DST, but according to the documentation and from looking at results for other time zones, it should instead store the moment of the transition and not the moment right before it).
1 parent 565e50e commit 15a7d59

File tree

7 files changed

+189
-43
lines changed

7 files changed

+189
-43
lines changed

core/nativeMain/cinterop/cpp/apple.mm

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -172,27 +172,31 @@ TZID timezone_by_name(const char *zone_name) {
172172
return id_by_name([NSString stringWithUTF8String: zone_name]);
173173
}
174174

175-
int offset_at_datetime(TZID zone_id, int64_t epoch_sec, int *offset) {
176-
*offset = INT_MAX;
177-
// timezone
178-
auto zone = timezone_by_id(zone_id);
179-
if (zone == nil) { return 0; }
180-
/* a date in an unspecified timezone, defined by the number of seconds since
181-
the start of the epoch in *that* unspecified timezone */
182-
auto date = dateWithTimeIntervalSince1970Saturating(epoch_sec);
183-
// The ISO8601 calendar.
175+
static NSDate *system_date_by_local_date(NSTimeZone *zone, NSDate *local_date) {
176+
// The Gregorian calendar.
184177
NSCalendar *iso8601 = [NSCalendar
185178
calendarWithIdentifier: NSCalendarIdentifierISO8601];
186-
if (iso8601 == nil) { return 0; }
179+
if (iso8601 == nil) { return nil; }
187180
// The UTC time zone
188181
NSTimeZone *utc = [NSTimeZone timeZoneForSecondsFromGMT: 0];
189182
/* Now, we say that the date that we initially meant is `date`, only with
190183
the context of being in a timezone `zone`. */
191184
NSDateComponents *dateComponents = [iso8601
192185
componentsInTimeZone: utc
193-
fromDate: date];
186+
fromDate: local_date];
194187
dateComponents.timeZone = zone;
195-
NSDate *newDate = [iso8601 dateFromComponents:dateComponents];
188+
return [iso8601 dateFromComponents:dateComponents];
189+
}
190+
191+
int offset_at_datetime(TZID zone_id, int64_t epoch_sec, int *offset) {
192+
*offset = INT_MAX;
193+
// timezone
194+
auto zone = timezone_by_id(zone_id);
195+
if (zone == nil) { return 0; }
196+
/* a date in an unspecified timezone, defined by the number of seconds since
197+
the start of the epoch in *that* unspecified timezone */
198+
NSDate *date = dateWithTimeIntervalSince1970Saturating(epoch_sec);
199+
NSDate *newDate = system_date_by_local_date(zone, date);
196200
if (newDate == nil) { return 0; }
197201
// we now know the offset of that timezone at this time.
198202
*offset = (int)[zone secondsFromGMTForDate: newDate];
@@ -203,5 +207,28 @@ int offset_at_datetime(TZID zone_id, int64_t epoch_sec, int *offset) {
203207
return result;
204208
}
205209

210+
int64_t at_start_of_day(TZID zone_id, int64_t epoch_sec) {
211+
// timezone
212+
auto zone = timezone_by_id(zone_id);
213+
if (zone == nil) { return INT_MAX; }
214+
NSDate *date = [NSDate dateWithTimeIntervalSince1970: epoch_sec];
215+
NSDate *newDate = system_date_by_local_date(zone, date);
216+
if (newDate == nil) { return INT_MAX; }
217+
int offset = (int)[zone secondsFromGMTForDate: newDate];
218+
/* if `epoch_sec` is not in the range supported by Darwin, assume that it
219+
is the correct local time for the midnight and just convert it to
220+
the system time. */
221+
if ([date timeIntervalSinceDate:[NSDate distantPast]] < 0 ||
222+
[date timeIntervalSinceDate:[NSDate distantFuture]] > 0)
223+
return epoch_sec - offset;
224+
// The ISO-8601 calendar.
225+
NSCalendar *iso8601 = [NSCalendar
226+
calendarWithIdentifier: NSCalendarIdentifierISO8601];
227+
iso8601.timeZone = zone;
228+
// start of the day denoted by `newDate`
229+
NSDate *midnight = [iso8601 startOfDayForDate: newDate];
230+
return (int64_t)([midnight timeIntervalSince1970]);
231+
}
232+
206233
}
207234
#endif // TARGET_OS_IPHONE

core/nativeMain/cinterop/cpp/cdate.cpp

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include "date/date.h"
1313
#include "date/tz.h"
1414
#include "helper_macros.hpp"
15+
#include <cstring>
1516
using namespace date;
1617
using namespace std::chrono;
1718

@@ -48,12 +49,7 @@ extern "C" {
4849
template <class T>
4950
static char * timezone_name(const T& zone)
5051
{
51-
auto name = zone.name();
52-
char * name_copy = check_allocation(
53-
(char *)malloc(sizeof(char) * (name.size() + 1)));
54-
name_copy[name.size()] = '\0';
55-
name.copy(name_copy, name.size());
56-
return name_copy;
52+
return strdup(zone.name().c_str());
5753
}
5854

5955
static const time_zone *zone_by_id(TZID id)
@@ -139,21 +135,33 @@ TZID timezone_by_name(const char *zone_name)
139135
}
140136
}
141137

142-
int offset_at_datetime(TZID zone_id, int64_t epoch_sec, int *offset)
138+
static int offset_at_datetime_impl(TZID zone_id, seconds sec, int *offset,
139+
GAP_HANDLING gap_handling)
143140
{
144141
try {
145142
auto zone = zone_by_id(zone_id);
146-
local_seconds seconds(saturating(epoch_sec));
143+
local_seconds seconds(sec);
147144
auto info = zone->get_info(seconds);
148145
switch (info.result) {
149146
case local_info::unique:
150147
*offset = info.first.offset.count();
151148
return 0;
152149
case local_info::nonexistent: {
153-
auto trans_duration = info.second.offset.count() -
154-
info.first.offset.count();
155150
*offset = info.second.offset.count();
156-
return trans_duration;
151+
switch (gap_handling) {
152+
case GAP_HANDLING_MOVE_FORWARD:
153+
return info.second.offset.count() -
154+
info.first.offset.count();
155+
case GAP_HANDLING_NEXT_CORRECT: {
156+
return duration_cast<std::chrono::seconds>(
157+
info.second.begin.time_since_epoch()).count() -
158+
sec.count() + info.second.offset.count();
159+
}
160+
default:
161+
// impossible
162+
*offset = INT_MAX;
163+
return 0;
164+
}
157165
}
158166
case local_info::ambiguous:
159167
if (info.second.offset.count() != *offset)
@@ -170,6 +178,25 @@ int offset_at_datetime(TZID zone_id, int64_t epoch_sec, int *offset)
170178
}
171179
}
172180

181+
int offset_at_datetime(TZID zone_id, int64_t epoch_sec, int *offset)
182+
{
183+
return offset_at_datetime_impl(zone_id, saturating(epoch_sec), offset,
184+
GAP_HANDLING_MOVE_FORWARD);
185+
}
186+
187+
int64_t at_start_of_day(TZID zone_id, int64_t epoch_sec)
188+
{
189+
int offset = 0;
190+
int trans = offset_at_datetime_impl(zone_id, saturating(epoch_sec), &offset,
191+
GAP_HANDLING_NEXT_CORRECT);
192+
if (offset == INT_MAX)
193+
return LONG_MAX;
194+
if (epoch_sec > max_available_instant || epoch_sec < min_available_instant) {
195+
trans = 0;
196+
}
197+
return epoch_sec - offset + trans;
198+
}
199+
173200
}
174201
#endif // !DATETIME_TARGET_WIN32
175202
#endif // !TARGET_OS_IPHONE

core/nativeMain/cinterop/cpp/windows.cpp

Lines changed: 74 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -212,23 +212,28 @@ static int64_t systemtime_to_unix_time(const SYSTEMTIME& systime)
212212
SECS_BETWEEN_1601_1970;
213213
}
214214

215+
struct TRANSITIONS_INFO {
216+
TIME_ZONE_INFORMATION tzi;
217+
SYSTEMTIME standard_local;
218+
SYSTEMTIME daylight_local;
219+
};
220+
215221
/* Checks whether the daylight saving time is in effect at the given time.
216222
`tzi` could be calculated here, but is passed along to avoid recomputing
217223
it. */
218224
static bool is_daylight_time(
219225
const DYNAMIC_TIME_ZONE_INFORMATION& dtzi,
220-
const TIME_ZONE_INFORMATION& tzi,
226+
TRANSITIONS_INFO& trans,
221227
const SYSTEMTIME& time)
222228
{
223229
// it means that daylight saving time is not supported at all
224-
if (tzi.StandardDate.wMonth == 0) {
230+
if (trans.tzi.StandardDate.wMonth == 0) {
225231
return false;
226232
}
227233
/* translate the "date" values stored in `tzi` into real dates of
228234
transitions to and from the daylight saving time. */
229-
SYSTEMTIME standard_local, daylight_local;
230-
get_transition_date(time.wYear, tzi.StandardDate, standard_local);
231-
get_transition_date(time.wYear, tzi.DaylightDate, daylight_local);
235+
get_transition_date(time.wYear, trans.tzi.StandardDate, trans.standard_local);
236+
get_transition_date(time.wYear, trans.tzi.DaylightDate, trans.daylight_local);
232237
/* Two things happen here:
233238
* All the relevant dates are converted to a number of ticks an some
234239
unified scale, counted in seconds. This is done so that we are able
@@ -239,10 +244,10 @@ static bool is_daylight_time(
239244
time, as seen by a person that is currently on the daylight saving
240245
time. So, in order for the dates to be on the same scale, the biases
241246
that are assumed to be currently active are negated. */
242-
int64_t standard = systemtime_to_ticks(standard_local) /
243-
WINDOWS_TICKS_PER_SEC + (tzi.Bias + tzi.DaylightBias) * 60;
244-
int64_t daylight = systemtime_to_ticks(daylight_local) /
245-
WINDOWS_TICKS_PER_SEC + (tzi.Bias + tzi.StandardBias) * 60;
247+
int64_t standard = systemtime_to_ticks(trans.standard_local) /
248+
WINDOWS_TICKS_PER_SEC + (trans.tzi.Bias + trans.tzi.DaylightBias) * 60;
249+
int64_t daylight = systemtime_to_ticks(trans.daylight_local) /
250+
WINDOWS_TICKS_PER_SEC + (trans.tzi.Bias + trans.tzi.StandardBias) * 60;
246251
int64_t time_secs = systemtime_to_ticks(time) /
247252
WINDOWS_TICKS_PER_SEC;
248253
/* Maybe `else` is never hit, but I've seen no indication of that assumption
@@ -258,18 +263,18 @@ static bool is_daylight_time(
258263

259264
// Get the UTC offset for a given timezone at a given time.
260265
static int offset_at_systime(DYNAMIC_TIME_ZONE_INFORMATION& dtzi,
266+
TRANSITIONS_INFO& ts,
261267
const SYSTEMTIME& systime)
262268
{
263-
TIME_ZONE_INFORMATION tzi{};
264-
bool result = GetTimeZoneInformationForYear(systime.wYear, &dtzi, &tzi);
269+
bool result = GetTimeZoneInformationForYear(systime.wYear, &dtzi, &ts.tzi);
265270
if (!result) {
266271
return INT_MAX;
267272
}
268-
auto bias = tzi.Bias;
269-
if (is_daylight_time(dtzi, tzi, systime)) {
270-
bias += tzi.DaylightBias;
273+
auto bias = ts.tzi.Bias;
274+
if (is_daylight_time(dtzi, ts, systime)) {
275+
bias += ts.tzi.DaylightBias;
271276
} else {
272-
bias += tzi.StandardBias;
277+
bias += ts.tzi.StandardBias;
273278
}
274279
return -bias * 60;
275280
}
@@ -330,7 +335,8 @@ int offset_at_instant(TZID zone_id, int64_t epoch_sec)
330335
}
331336
SYSTEMTIME systime;
332337
unix_time_to_systemtime(epoch_sec, systime);
333-
return offset_at_systime(dtzi, systime);
338+
TRANSITIONS_INFO ts{};
339+
return offset_at_systime(dtzi, ts, systime);
334340
}
335341

336342
TZID timezone_by_name(const char *zone_name)
@@ -344,7 +350,8 @@ TZID timezone_by_name(const char *zone_name)
344350
}
345351
}
346352

347-
int offset_at_datetime(TZID zone_id, int64_t epoch_sec, int *offset)
353+
static int offset_at_datetime_impl(TZID zone_id, int64_t epoch_sec, int *offset,
354+
GAP_HANDLING gap_handling)
348355
{
349356
DYNAMIC_TIME_ZONE_INFORMATION dtzi{};
350357
bool result = time_zone_by_id(zone_id, dtzi);
@@ -354,7 +361,8 @@ int offset_at_datetime(TZID zone_id, int64_t epoch_sec, int *offset)
354361
SYSTEMTIME localtime, utctime, adjusted;
355362
unix_time_to_systemtime(epoch_sec, localtime);
356363
TzSpecificLocalTimeToSystemTimeEx(&dtzi, &localtime, &utctime);
357-
*offset = offset_at_systime(dtzi, utctime);
364+
TRANSITIONS_INFO trans{};
365+
*offset = offset_at_systime(dtzi, trans, utctime);
358366
SystemTimeToTzSpecificLocalTimeEx(&dtzi, &utctime, &adjusted);
359367
/* We don't use `epoch_sec` instead of `systemtime_to_unix_time(localtime)
360368
because `unix_time_to_systemtime(epoch_sec, localtime)` above could
@@ -364,8 +372,55 @@ int offset_at_datetime(TZID zone_id, int64_t epoch_sec, int *offset)
364372
result to return: timezone database information outside of [1970; current
365373
time) is not accurate anyway, and WinAPI supports dates in years [1601;
366374
30827], which should be enough for all practical purposes. */
367-
return (int)(systemtime_to_unix_time(adjusted) -
375+
const auto transition_duration = (int)(systemtime_to_unix_time(adjusted) -
368376
systemtime_to_unix_time(localtime));
377+
if (transition_duration == 0)
378+
return 0;
379+
switch (gap_handling) {
380+
case GAP_HANDLING_MOVE_FORWARD:
381+
return transition_duration;
382+
case GAP_HANDLING_NEXT_CORRECT:
383+
/* Let x, y in {daylight, standard}
384+
If a gap happened, then
385+
xEnd + xOffset < utctime < yBegin + yOffset
386+
What we need to return is
387+
yBegin + yOffset - epoch_sec
388+
To learn whether we crossed from daylight to standard or vice versa:
389+
xEnd = yBegin - epsilon => yOffset + epsilon > xOffset
390+
Thus, we crossed from the lower offset to the bigger one. So,
391+
return (daylight.offset > standard.offset ?
392+
daylight.begin + daylight.offset :
393+
standard.begin + standard.offset) - epoch_sec */
394+
if (trans.tzi.DaylightBias < trans.tzi.StandardBias) {
395+
return systemtime_to_unix_time(trans.daylight_local)
396+
+ trans.tzi.StandardBias - trans.tzi.DaylightBias
397+
- epoch_sec + 1;
398+
} else {
399+
return systemtime_to_unix_time(trans.standard_local)
400+
+ trans.tzi.DaylightBias - trans.tzi.StandardBias
401+
- epoch_sec + 1;
402+
}
403+
default:
404+
// impossible
405+
*offset = INT_MAX;
406+
return 0;
407+
}
408+
}
409+
410+
int offset_at_datetime(TZID zone_id, int64_t epoch_sec, int *offset)
411+
{
412+
return offset_at_datetime_impl(zone_id, epoch_sec, offset,
413+
GAP_HANDLING_MOVE_FORWARD);
414+
}
415+
416+
int64_t at_start_of_day(TZID zone_id, int64_t epoch_sec)
417+
{
418+
int offset = 0;
419+
int trans = offset_at_datetime_impl(zone_id, epoch_sec, &offset,
420+
GAP_HANDLING_NEXT_CORRECT);
421+
if (offset == INT_MAX)
422+
return LONG_MAX;
423+
return epoch_sec - offset + trans;
369424
}
370425

371426
}

core/nativeMain/cinterop/public/cdate.h

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@
88
#include <stddef.h>
99

1010
typedef size_t TZID;
11-
const size_t TZID_INVALID = SIZE_MAX;
11+
const TZID TZID_INVALID = SIZE_MAX;
12+
13+
enum GAP_HANDLING {
14+
GAP_HANDLING_MOVE_FORWARD,
15+
GAP_HANDLING_NEXT_CORRECT,
16+
};
1217

1318
/* Returns a string that must be freed by the caller, or null.
1419
If something is returned, `id` has the id of the timezone. */
@@ -31,3 +36,5 @@ TZID timezone_by_name(const char *zone_name);
3136
case the time does not exist, having fallen in the gap.
3237
In case of an error, "offset" is set to INT_MAX. */
3338
int offset_at_datetime(TZID zone, int64_t epoch_sec, int *offset);
39+
40+
int64_t at_start_of_day(TZID zone, int64_t midnight_epoch_sec);

core/nativeMain/cinterop_actuals/TimeZoneNative.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ internal actual fun available_zone_ids(): kotlinx.cinterop.CPointer<kotlinx.cint
2222
internal actual fun offset_at_datetime(zone: kotlinx.datetime.TZID /* = kotlin.ULong */, epoch_sec: platform.posix.int64_t /* = kotlin.Long */, offset: kotlinx.cinterop.CValuesRef<kotlinx.cinterop.IntVar /* = kotlinx.cinterop.IntVarOf<kotlin.Int> */>?): kotlin.Int =
2323
kotlinx.datetime.internal.offset_at_datetime(zone, epoch_sec, offset)
2424

25+
internal actual fun at_start_of_day(zone: kotlinx.datetime.TZID /* = kotlin.ULong */, epoch_sec: platform.posix.int64_t /* = kotlin.Long */): kotlin.Long =
26+
kotlinx.datetime.internal.at_start_of_day(zone, epoch_sec)
27+
2528
internal actual fun offset_at_instant(zone: kotlinx.datetime.TZID /* = kotlin.ULong */, epoch_sec: platform.posix.int64_t /* = kotlin.Long */): kotlin.Int =
2629
kotlinx.datetime.internal.offset_at_instant(zone, epoch_sec)
2730

core/nativeMain/src/TimeZone.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ internal typealias TZID = platform.posix.size_t
1919
internal expect val TZID_INVALID: TZID
2020
internal expect fun available_zone_ids(): kotlinx.cinterop.CPointer<kotlinx.cinterop.CPointerVar<kotlinx.cinterop.ByteVar>>?
2121
internal expect fun offset_at_datetime(zone: kotlinx.datetime.TZID /* = kotlin.ULong */, epoch_sec: platform.posix.int64_t /* = kotlin.Long */, offset: kotlinx.cinterop.CValuesRef<kotlinx.cinterop.IntVar /* = kotlinx.cinterop.IntVarOf<kotlin.Int> */>?): kotlin.Int
22+
internal expect fun at_start_of_day(zone: kotlinx.datetime.TZID /* = kotlin.ULong */, epoch_sec: platform.posix.int64_t /* = kotlin.Long */): kotlin.Long
2223
internal expect fun offset_at_instant(zone: kotlinx.datetime.TZID /* = kotlin.ULong */, epoch_sec: platform.posix.int64_t /* = kotlin.Long */): kotlin.Int
2324
internal expect fun timezone_by_name(zone_name: kotlin.String?): kotlinx.datetime.TZID /* = kotlin.ULong */
2425

@@ -100,6 +101,16 @@ public actual open class TimeZone internal constructor(private val tzid: TZID, a
100101

101102
actual fun LocalDateTime.toInstant(): Instant = atZone().toInstant()
102103

104+
internal open fun atStartOfDay(date: LocalDate): Instant = memScoped {
105+
val ldt = LocalDateTime(date, LocalTime.MIN)
106+
val epochSeconds = ldt.toEpochSecond(ZoneOffset.UTC)
107+
val midnightInstantSeconds = at_start_of_day(tzid, epochSeconds)
108+
if (midnightInstantSeconds == Long.MAX_VALUE) {
109+
throw RuntimeException("Unable to acquire the time of start of day at $date for zone $this")
110+
}
111+
Instant(midnightInstantSeconds, 0)
112+
}
113+
103114
internal open fun LocalDateTime.atZone(preferred: ZoneOffset? = null): ZonedDateTime = memScoped {
104115
val epochSeconds = toEpochSecond(ZoneOffset.UTC)
105116
val offset = alloc<IntVar>()
@@ -250,6 +261,9 @@ public actual class ZoneOffset internal constructor(actual val totalSeconds: Int
250261
}
251262
}
252263

264+
internal override fun atStartOfDay(date: LocalDate): Instant =
265+
LocalDateTime(date, LocalTime.MIN).atZone(null).toInstant()
266+
253267
internal override fun LocalDateTime.atZone(preferred: ZoneOffset?): ZonedDateTime =
254268
ZonedDateTime(this@atZone, this@ZoneOffset, this@ZoneOffset)
255269

0 commit comments

Comments
 (0)