Определите, какая часть интервала перекрывает данный день недели в данном часовом поясе - PullRequest
0 голосов
/ 11 мая 2018

У меня есть глобальный временной интервал (от одной «временной метки» UTC до другого), и я хочу определить, какая часть интервала перекрывает данный день недели в данном часовом поясе.

Давайте рассмотрим пример: скажем, интервал 2018-05-11T02:00:00Z/2018-05-11T10:00:00Z, а день недели Пятница .

Для Нью-Йорка (Америка / Нью-Йорк) интервал переводится в местный интервал времени даты 2018-05-10T22:00/2018-05-11T06:00, где первые два часа интервала не перекрываются в пятницу. Таким образом, полученный интервал должен составлять 2018-05-11T04:00:00Z/2018-05-11T10:00:00Z. Если бы часовым поясом был Копенгаген (Европа / Копенгаген), исходный интервал остался бы неизменным, поскольку все они перекрываются в пятницу в этом часовом поясе.

Если интервал был достаточно длинным, вы могли бы легко иметь несколько совпадений с днем ​​недели. Конечно, также возможно отсутствие совпадений.

Мне трудно найти лучший и самый надежный способ решить эту проблему. Моя лучшая идея - взять день недели и перевести его в глобальное время из заданного часового пояса, а затем проверить на совпадения. Однако день недели не привязан к конкретной дате, а это значит, что мне на самом деле нечего переводить, и поэтому сначала нужно выяснить, какими потенциально перекрывающимися датами будет день недели.

Если я переведу интервал в местное время, возьму совпадение с днем ​​недели (намного проще, потому что у меня теперь есть фактические даты для работы), а затем переведу их обратно, я получу правильный ответ в большинстве случаев. Однако такие вещи, как переходы DST, могут легко испортить ситуацию при переводе назад, а откат может привести к тому, что локальный временной интервал даты станет «недействительным», то есть начало будет после конца, открыв еще одну банку с червями.

Я пытаюсь решить проблему в C # с помощью NodaTime, но думаю, что проблема носит общий характер.

Ниже приведена пара тестов, запрошенных @jskeet:

using FluentAssertions;
using NodaTime;
using System;
using System.Collections.Generic;
using System.Linq;
using Xunit;

public class Tests
{
    public static TheoryData<Interval, IsoDayOfWeek, DateTimeZone, IEnumerable<Interval>> OverlapsDayOfWeekExamples = new TheoryData<Interval, IsoDayOfWeek, DateTimeZone, IEnumerable<Interval>>
    {
        {   // No overlap in given time zone
            new Interval(Instant.FromUtc(2018, 05, 11, 00, 00), Instant.FromUtc(2018, 05, 11, 04, 00)),
            IsoDayOfWeek.Friday,
            DateTimeZoneProviders.Tzdb["America/New_York"],
            Enumerable.Empty<Interval>()
        },
        {   // Cut short because interval begins Thursday
            new Interval(Instant.FromUtc(2018, 05, 11, 02, 00), Instant.FromUtc(2018, 05, 11, 10, 00)),
            IsoDayOfWeek.Friday,
            DateTimeZoneProviders.Tzdb["America/New_York"],
            new [] { new Interval(Instant.FromUtc(2018, 05, 11, 04, 00), Instant.FromUtc(2018, 05, 11, 10, 00)) }
        },
        {   // Remains unchanged because everything overlaps in given time zone
            new Interval(Instant.FromUtc(2018, 05, 11, 02, 00), Instant.FromUtc(2018, 05, 11, 10, 00)),
            IsoDayOfWeek.Friday,
            DateTimeZoneProviders.Tzdb["Europe/Copenhagen"],
            new [] { new Interval(Instant.FromUtc(2018, 05, 11, 02, 00), Instant.FromUtc(2018, 05, 11, 10, 00)) }
        },
        {   // Cut short because interval begins Saturday and day starts at 01:00 (Spring forward)
            new Interval(Instant.FromUtc(2018, 11, 04, 02, 15), Instant.FromUtc(2018, 11, 04, 06, 30)),
            IsoDayOfWeek.Sunday,
            DateTimeZoneProviders.Tzdb["America/Sao_Paulo"],
            new [] { new Interval(Instant.FromUtc(2018, 11, 04, 03, 00), Instant.FromUtc(2018, 11, 04, 06, 30)) }
        },
        {   // Cut short because interval begins Saturday and day starts later (Fall back)
            new Interval(Instant.FromUtc(2018, 02, 18, 01, 00), Instant.FromUtc(2018, 02, 18, 07, 30)),
            IsoDayOfWeek.Sunday,
            DateTimeZoneProviders.Tzdb["America/Sao_Paulo"],
            new [] { new Interval(Instant.FromUtc(2018, 02, 18, 03, 00), Instant.FromUtc(2018, 02, 18, 07, 30)) }
        },
        {   // Overlaps multiple times (middle overlap is during DST transition)
            new Interval(Instant.FromUtc(2018, 10, 28, 16, 15), Instant.FromUtc(2018, 11, 11, 12, 30)),
            IsoDayOfWeek.Sunday,
            DateTimeZoneProviders.Tzdb["America/New_York"],
            new []
            {
                new Interval(Instant.FromUtc(2018, 10, 28, 16, 15), Instant.FromUtc(2018, 10, 29, 04, 00)),
                new Interval(Instant.FromUtc(2018, 11, 04, 04, 00), Instant.FromUtc(2018, 11, 05, 05, 00)),
                new Interval(Instant.FromUtc(2018, 11, 11, 05, 00), Instant.FromUtc(2018, 11, 11, 12, 30)),
            }
        },
        {   // Results in an invalid date time interval
            new Interval(Instant.FromUtc(2018, 10, 28, 00, 45), Instant.FromUtc(2018, 10, 28, 01, 15)),
            IsoDayOfWeek.Sunday,
            DateTimeZoneProviders.Tzdb["Europe/Copenhagen"],
            new [] { new Interval(Instant.FromUtc(2018, 10, 28, 00, 45), Instant.FromUtc(2018, 10, 28, 01, 15)) }
        },
    };

    [Theory]
    [MemberData(nameof(OverlapsDayOfWeekExamples))]
    public void OverlapsDayOfWeekTest531804504(Interval interval, IsoDayOfWeek dayOfWeek, DateTimeZone timeZone, IEnumerable<Interval> expected)
    {
        OverlapsDayOfWeek(interval, dayOfWeek, timeZone).Should().BeEquivalentTo(expected);
    }

    public IEnumerable<Interval> OverlapsDayOfWeek(Interval interval, IsoDayOfWeek dayOfWeek, DateTimeZone timeZone)
    {
        throw new NotImplementedException();
    }
}

1 Ответ

0 голосов
/ 11 мая 2018

В тестах есть несколько проблем, но после их небольшого изменения они проходят с кодом ниже.В принципе, это вопрос:

  • Возьмите интервал и определите даты, в которые может находиться внутри него для любого часового пояса.Мы можем просто преобразовать его в интервал дат в UTC и увеличить его на пару дней в каждом направлении.Выходные данные: последовательность дат.
  • Для каждой даты в этой последовательности дат преобразуйте ее в интервал в целевой зоне: начало - это начало дня в этой зоне;конец - начало следующего дня в этой зоне.(Это будет обрабатывать переходы DST.) Выход: последовательность интервалов.
  • Для каждого интервала в пределах этой последовательности интервалов, определить пересечение между этим и входным интервалом.Вывод: последовательность интервалов, некоторые из которых могут быть нулевыми (для «без пересечения»)
  • Результатом являются ненулевые интервалы в последовательности.

Вот код, демонстрирующийчто:

using System;
using System.Collections.Generic;
using System.Linq;
using NodaTime;

public class Program 
{
    public static void Main() 
    {
        var start = Instant.FromUtc(2018, 5, 11, 2, 0);
        var end = Instant.FromUtc(2018, 5, 11, 10, 0);
        var input = new Interval(start, end);

        DisplayDayIntervals(input, "America/New_York", IsoDayOfWeek.Friday);
        DisplayDayIntervals(input, "Europe/Copenhagen", IsoDayOfWeek.Friday);
    }

    static void DisplayDayIntervals(Interval input, string zoneId, IsoDayOfWeek dayOfWeek)
    {
        var zone = DateTimeZoneProviders.Tzdb[zoneId];
        var intervals = GetDayIntervals(input, zone, dayOfWeek);
        Console.WriteLine($"{zoneId}: [{string.Join(", ", intervals)}]");
    }

    public static IEnumerable<Interval> GetDayIntervals(
        Interval input,
        DateTimeZone zone,
        IsoDayOfWeek dayOfWeek)
    {
        // Get a range of dates that covers the input interval. This is deliberately
        // larger than it may need to be, to handle days starting at different instants
        // in different time zones. 
        LocalDate startDate = input.Start.InZone(DateTimeZone.Utc).Date.PlusDays(-2);
        LocalDate endDate = input.End.InZone(DateTimeZone.Utc).Date.PlusDays(2);        
        var dates = GetDates(startDate, endDate, dayOfWeek);

        // Convert those dates into intervals, each of which may or may not overlap
        // with our input.
        var intervals = dates.Select(date => GetIntervalForDate(date, zone));

        // Find the intersection of each date-interval with our input, and discard
        // any non-overlaps
        return intervals.Select(dateInterval => Intersect(dateInterval, input))
                        .Where(x => x != null)
                        .Select(x => x.Value);
    }

    private static IEnumerable<LocalDate> GetDates(LocalDate start, LocalDate end, IsoDayOfWeek dayOfWeek)
    {
        for (var date = start.With(DateAdjusters.NextOrSame(dayOfWeek));
             date <= end;
             date = date.With(DateAdjusters.Next(dayOfWeek)))
         {
             yield return date;
         }
    }

    private static Interval GetIntervalForDate(LocalDate date, DateTimeZone zone)
    {
        var start = date.AtStartOfDayInZone(zone).ToInstant();
        var end = date.PlusDays(1).AtStartOfDayInZone(zone).ToInstant();
        return new Interval(start, end);
    }

    private static Interval? Intersect(Interval left, Interval right)
    {
        Instant start = Instant.Max(left.Start, right.Start);
        Instant end = Instant.Min(left.End, right.End);
        return start < end ? new Interval(start, end) : (Interval?) null;
    }
}
...