Лучший способ сравнить периоды с помощью NodaTime (или альтернативы)

У меня есть требование иметь относительную минимальную/максимальную проверку даты, которую можно сохранить в базе данных, чтобы настроить приложение для каждого клиента. Я решил, что NodaTime.Period из-за возможности указывать годы был лучшим выбором. Однако NodaTime.Period не позволяет сравнивать себя с другим периодом.

Пример данных, предоставленных для этой проверки:

  • Минимальный возраст 18 лет.
  • Максимальный возраст o 100 лет.
  • Минимальный срок продажи 1 месяц
  • Максимальная продолжительность продажи 3 месяца
  • Минимальная рекламная кампания 7 дней

(Примечание. Текущие требования заключаются в том, что год/месяц/день не будут объединяться при проверке)

Валидации:

public Period RelativeMinimum { get; set; }
public Period RelativeMaximum { get; set; }

Учитывая введенную пользователем дату (и сейчас):

var now = new LocalDate(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day);
var userValue = new LocalDate(date.Year, date.Month, date.Day);
var difference = Period.Between(userValue, now);

У меня есть сравнение:

if(RelativeMinimum != null && difference.IsLessThan(RelativeMinimum))))
{
    response.IsValid = false;
    response.Errors.Add(MinimumErrorMessage);
}

Который потребляет класс расширений:

public static class PeriodExtensions
{
    public static bool IsLessThan(this Period p, Period p2)
    {
        return (p.Years < p2.Years) || (p.Years == p2.Years && p.Months < p2.Months) || (p.Years == p2.Years && p.Months == p2.Months && p.Days < p2.Days);
    }

    public static bool IsGreaterThan(this Period p, Period p2)
    {
        return (p.Years > p2.Years) || (p.Years == p2.Years && p.Months > p2.Months) || (p.Years == p2.Years && p.Months == p2.Months && p.Days > p2.Days);
    }
}

Хотя этот подход работает, учитывая условия тестирования, которые у меня есть, я должен задаться вопросом, почему @jon-skeet не реализовал это, и сразу же должен беспокоиться о том, что я упускаю и какую альтернативу я должен использовать вместо этого?


person VulgarBinary    schedule 13.05.2015    source источник


Ответы (2)


Основная причина, по которой периоды несопоставимы, заключается в том, что они могут содержать компоненты переменной длины.

Два месячных периода не обязательно имеют одинаковое количество дней. Например, что больше: 1 месяц или 30 дней? Если месяц январь, то это больше, чем 30 дней. Если месяц февраль, то меньше 30 дней.

То же самое относится и к годам. У кого-то 365 дней, у кого-то 366.

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

Что касается кода:

  • Если вы хотите LocalDate из DateTime, используйте LocalDateTime.FromDateTime(dt).Date

  • Чтобы получить текущую дату, используйте SystemClock.Instance.Now.InZone(tz).Date

    • If you intended that to be the same as DateTime.Now, which uses the local time zone of the computer where the code is running, then get tz by calling DateTimeZoneProviders.Tzdb.GetSystemDefault()
  • Для сравнения типа проблемы, которую вы описали, рассмотрите возможность определения минимального и максимального дней вместо минимального и максимального периодов. Тогда у вас не будет такой вариации юнитов. Вы можете получить разницу в днях следующим образом:

    long days = Period.Between(d1, d2, PeriodUnits.Days).Days;
    

Я считаю, что что-то вроде этого будет хорошо работать для вашего варианта использования:

public static bool IsDifferenceLessThan(LocalDate d1, LocalDate d2, Period p)
{
    if (p.HasTimeComponent)
        throw new ArgumentException("Can only compare dates.", "p");

    if (p.Years != 0)
    {
        if (p.Months != 0 || p.Weeks != 0 || p.Days != 0)
            throw new ArgumentException("Can only compare one component of a period.", "p");

        var years = Period.Between(d1, d2, PeriodUnits.Years).Years;
        return years < p.Years;
    }

    if (p.Months != 0)
    {
        if (p.Weeks != 0 || p.Days != 0)
            throw new ArgumentException("Can only compare one component of a period.", "p");

        var months = Period.Between(d1, d2, PeriodUnits.Months).Months;
        return months < p.Months;
    }

    if (p.Weeks != 0)
    {
        if (p.Days != 0)
            throw new ArgumentException("Can only compare one component of a period.", "p");

        var weeks = Period.Between(d1, d2, PeriodUnits.Weeks).Weeks;
        return weeks < p.Weeks;
    }

    var days = Period.Between(d1, d2, PeriodUnits.Days).Days;
    return days < p.Days;
}
person Matt Johnson-Pint    schedule 13.05.2015
comment
К сожалению, требование клиента состоит в том, чтобы иметь возможность говорить такие вещи, как: минимум 13 лет, минимальная продолжительность продажи 1 месяц или продолжительность кампании 2 дня... Я обновлю свой вопрос, чтобы учесть это. Сравнение только за день, к сожалению, не получится, но пост все равно был очень информативным! - person VulgarBinary; 14.05.2015
comment
Вы все еще можете использовать подход, который я описал в последнем пункте. Вам просто нужно будет изменить единицы периода в зависимости от проверки, которую вы пытаетесь сделать. Например, ...PeriodUnits.Months).Months или ...PeriodUnits.Years).Years. - person Matt Johnson-Pint; 14.05.2015
comment
Итак, если бы я написал способ проверить единицу периода, сказав Год != 0 -> Год или Месяц != 0 -> Месяц... и т. д. Будет ли этого достаточно? (На основе RelativeMinimum или RelativeMaximum) - person VulgarBinary; 14.05.2015
comment
По сути, да. Смотрите обновление, которое я разместил. Я думаю, вы можете экстраполировать другие методы оттуда. Ключевая часть заключается в том, что операции Between сообщается, какой тип календарного компонента вы ищете. - person Matt Johnson-Pint; 14.05.2015
comment
Спасибо, Мэтт! Идеальное понимание, в котором я нуждался в этом :-) Очень признателен! - person VulgarBinary; 14.05.2015

В качестве дополнения к уже отличному ответу Мэтта мы предоставляем возможность создания IComparer<Period> с определенной точкой привязки, например.

var febComparer = Period.CreateComparer(new LocalDate(2015, 2, 1).AtMidnight());
var marchComparer = Period.CreateComparer(new LocalDate(2015, 3, 1).AtMidnight());
var oneMonth = Period.FromMonths(1);
var twentyNineDays = Period.FromDays(29);

// -1: Feb 1st + 1 month is earlier than Feb 1st + 29 days
Console.WriteLine(febComparer.Compare(oneMonth, twentyNineDays));
// 1: March 1st + 1 month is later than March 1st + 29 days
Console.WriteLine(marchComparer.Compare(oneMonth, twentyNineDays));
person Jon Skeet    schedule 14.05.2015
comment
Я смотрел на Comparer и не думал, что это действительно может решить то, что мне нужно. Тем не менее, приятно понимать, что и как работает CreateComparer, это может предоставить другой альтернативный подход. (Я знаю RTFM... Спасибо, @jon-skeet!!) - person VulgarBinary; 14.05.2015
comment
@VulgarBinary: Да, я добавил этот ответ в основном для других... Я не думаю, что это правильное решение для вашего варианта использования, но может быть правильным решением для тех, кто хочет сравнить периоды в простой способ. - person Jon Skeet; 14.05.2015
comment
Ага! Документация по API на сайте nodatime.org меня не полностью устроила, но это помогло. Я уверен, что другие, оглядываясь вокруг, найдут это чрезвычайно полезным. :-) - person VulgarBinary; 14.05.2015
comment
Хороший. Я не знал об этом конкретном лакомом кусочке. :) - person Matt Johnson-Pint; 14.05.2015