使用 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