使用 Noda Time 从本地日期和时区查找最接近的有效全球日期
Find closest valid global date from local date and time zone using Noda Time
给定一个时区,我需要检查特定时刻是否包含在日期范围和本地时间范围内。假设时区为 Europe/Paris
,日期范围为 2020-03-01 to 2020-03-31
,本地时间范围为 02:30 to 03:30
。因此,在此示例中,目标是查明给定时刻是否发生在 2020 年 3 月的任何一天 02:30 和 03:30 之间。
事情是,在这个时区的 2020-03-29
日,夏令时在 02:00
应用并且时钟跳到 03:00
,所以本地时间范围 02:00 to 03:00
实际上并不存在。
我想通过将这个特定日期的时间范围移动到 03:00 to 03:30
来处理这种情况,03:00
是在给定时区的 02:30
之前最接近的有效时间。我知道 DateTimeZone.AtLeniently(LocalDateTime)
,但它没有按预期工作:
var localDate = new LocalDateTime(2020, 03, 29, 02, 30);
DateTimeZone timeZone = DateTimeZoneProviders.Tzdb["Europe/Paris"];
ZonedDateTime globalDate = timeZone.AtLeniently(localDate);
Console.WriteLine(globalDate); // 2020-03-29T03:30:00 Europe/Paris (+02)
^
如您所见,AtLeniently
将时间偏移 1 整小时(向前偏移“间隙”的持续时间,如 the documentation 中所述),因此解析的日期是 03:30
而不是 03:00
。我不想将无效时间移动 1 整小时,我更愿意向前移动到最近的有效时间(例如 03:00
)。
解决这个问题的一个方法是换个角度看问题。不是将 [day + local time range + time zone] 转换为全球时间范围并将其与给定时刻进行比较,我们可以将时刻转换为该时区的本地日期时间,并将其与本地值进行比较。
DateTimeZone dtz = DateTimeZoneProviders.Tzdb[ianaTimeZone];
var globalInstant = Instant.FromUtc(2020, 03, 29, 01, 15); // 2020-03-29T03:15:00 Europe/Paris (+02)
LocalDateTime localDate = globalInstant.InZone(dtz).LocalDateTime;
var localStartDate = new LocalDateTime(2020, 03, 29, 02, 30);
var localEndDate = new LocalDateTime(2020, 03, 29, 03, 30);
Console.WriteLine(localDate > localStartDate && localDate < localEndDate); // true
根据 Jon Skeet 的评论,我决定使用自定义 ZoneLocalMappingResolver
。这是我们当前的实现:
private ZonedDateTime ResolveLocal(
LocalDateTime localDate, DateTimeZone timeZone
)
{
return timeZone.ResolveLocal(
localDate,
resolver: mapping => mapping.Count == 0 ?
// Handle the case where the specified local date is skipped in
// the time zone by using the next valid date.
mapping.LateInterval.Start.InZone(mapping.Zone) :
// Otherwise, use the last available result (single if
// unambiguous).
mapping.Last()
);
}
private bool IsInInterval(
Instant instant, LocalDateTime start, LocalDateTime end,
DateTimeZone timeZone
)
{
ZonedDateTime globalStart = this.ResolveLocal(start, timeZone);
ZonedDateTime globalEnd = this.ResolveLocal(end, timeZone);
return instant > globalStart.ToInstant() &&
instant < globalEnd.ToInstant();
}
然后:
DateTimeZone timeZone = DateTimeZoneProviders.Tzdb["Europe/Paris"];
// instant matches 2020-03-29T03:15:00 Europe/Paris (+02)
var instant = Instant.FromUtc(2020, 03, 29, 01, 15);
// Interval bounds
// start will be translated to 2020-03-29T03:00:00 Europe/Paris (+02)
// end will be translated to 2020-03-29T03:30:00 Europe/Paris (+02)
var start = new LocalDateTime(2020, 03, 29, 02, 30);
var end = new LocalDateTime(2020, 03, 29, 03, 30);
bool isInInterval = this.IsInInterval(instant, start, end, timeZone);
Console.WriteLine(isInInterval); // true
给定一个时区,我需要检查特定时刻是否包含在日期范围和本地时间范围内。假设时区为 Europe/Paris
,日期范围为 2020-03-01 to 2020-03-31
,本地时间范围为 02:30 to 03:30
。因此,在此示例中,目标是查明给定时刻是否发生在 2020 年 3 月的任何一天 02:30 和 03:30 之间。
事情是,在这个时区的 2020-03-29
日,夏令时在 02:00
应用并且时钟跳到 03:00
,所以本地时间范围 02:00 to 03:00
实际上并不存在。
我想通过将这个特定日期的时间范围移动到 03:00 to 03:30
来处理这种情况,03:00
是在给定时区的 02:30
之前最接近的有效时间。我知道 DateTimeZone.AtLeniently(LocalDateTime)
,但它没有按预期工作:
var localDate = new LocalDateTime(2020, 03, 29, 02, 30);
DateTimeZone timeZone = DateTimeZoneProviders.Tzdb["Europe/Paris"];
ZonedDateTime globalDate = timeZone.AtLeniently(localDate);
Console.WriteLine(globalDate); // 2020-03-29T03:30:00 Europe/Paris (+02)
^
如您所见,AtLeniently
将时间偏移 1 整小时(向前偏移“间隙”的持续时间,如 the documentation 中所述),因此解析的日期是 03:30
而不是 03:00
。我不想将无效时间移动 1 整小时,我更愿意向前移动到最近的有效时间(例如 03:00
)。
解决这个问题的一个方法是换个角度看问题。不是将 [day + local time range + time zone] 转换为全球时间范围并将其与给定时刻进行比较,我们可以将时刻转换为该时区的本地日期时间,并将其与本地值进行比较。
DateTimeZone dtz = DateTimeZoneProviders.Tzdb[ianaTimeZone];
var globalInstant = Instant.FromUtc(2020, 03, 29, 01, 15); // 2020-03-29T03:15:00 Europe/Paris (+02)
LocalDateTime localDate = globalInstant.InZone(dtz).LocalDateTime;
var localStartDate = new LocalDateTime(2020, 03, 29, 02, 30);
var localEndDate = new LocalDateTime(2020, 03, 29, 03, 30);
Console.WriteLine(localDate > localStartDate && localDate < localEndDate); // true
根据 Jon Skeet 的评论,我决定使用自定义 ZoneLocalMappingResolver
。这是我们当前的实现:
private ZonedDateTime ResolveLocal(
LocalDateTime localDate, DateTimeZone timeZone
)
{
return timeZone.ResolveLocal(
localDate,
resolver: mapping => mapping.Count == 0 ?
// Handle the case where the specified local date is skipped in
// the time zone by using the next valid date.
mapping.LateInterval.Start.InZone(mapping.Zone) :
// Otherwise, use the last available result (single if
// unambiguous).
mapping.Last()
);
}
private bool IsInInterval(
Instant instant, LocalDateTime start, LocalDateTime end,
DateTimeZone timeZone
)
{
ZonedDateTime globalStart = this.ResolveLocal(start, timeZone);
ZonedDateTime globalEnd = this.ResolveLocal(end, timeZone);
return instant > globalStart.ToInstant() &&
instant < globalEnd.ToInstant();
}
然后:
DateTimeZone timeZone = DateTimeZoneProviders.Tzdb["Europe/Paris"];
// instant matches 2020-03-29T03:15:00 Europe/Paris (+02)
var instant = Instant.FromUtc(2020, 03, 29, 01, 15);
// Interval bounds
// start will be translated to 2020-03-29T03:00:00 Europe/Paris (+02)
// end will be translated to 2020-03-29T03:30:00 Europe/Paris (+02)
var start = new LocalDateTime(2020, 03, 29, 02, 30);
var end = new LocalDateTime(2020, 03, 29, 03, 30);
bool isInInterval = this.IsInInterval(instant, start, end, timeZone);
Console.WriteLine(isInInterval); // true