Работа с java.time в Kotlin: любовь, боль, страдания

Микропост о том, как можно себя обмануть при использовании фичи Котлин: возможность работы с операторами сравнения типа Comparable.

Кто юзает Котлин не могли не оценить перегрузку операторов (ну точнее как она сделана), правда я допустим жил в Java и без нее прекрасно, но да тут не об этой фичи языка, а об основанной на ней: Comparison Operations. Это когда можно применять знаки сравнения для классов, реализующих Comparable, что является сахаром, но очень приятным.

И так, поупражняемся на синтетике: у нас есть какой-либо временной отрезок, т.е. начальная и конечная дата время и нам нужно проверить факт пересечения временных отрезков.

На Java (пишу максимально кратко и без принятых норм, просто передать идею):

class TimeIterval {
  LocalDateTime from;
  LocalDateTime to;
}
class TimeIntervalUtil {
  public boolean areOverlapped(TimeInterval first, TimeInterval second) {
            return (first.from.isEqual(second.to) || first.from.isBefore(second.to)) &&
                (first.to.isEqual(second.from) || first.to.isAfter(second.from));
  }
}

т.е. ничего сложного, но на всякий случай поясню код (мне приходиться самому пояснять себе такой код, когда его встречаю): пересечение двух интервалов возможно только если дата начала одного наступила ранее или в тот же момент времени по отношению к дате окончания второго и в это же время дата окончания первого интервала наступает позднее или в момент начала второго.

Теперь то же самое но на Котлине с его сахаром, но без рейнджей:

data class TimeInterval(val from: LocalDateTime, val to: LocalDateTime)
fun areOverlapped(first: TimeInterval, second: TimeInterval): Boolean = 
  first.from <= second.to && first.to >= second.from

Ну я думаю без комментариев где видно лучше, что приятней использовать и быстрее понимать. Довольные переходим на Котлин и начинаем работать по аналогии уже с OffsetDateTime.

Тут нам нужно сделать то же самое, но уже с OffsetDateTime. Он тоже Comparable, как и почти все в java.time. Следовательно мы будем использовать такие же подходы как и LocalDateTime. В частности на Java код точно не измениться и будет работать так же как и ранее, а вот с Котлином будет засада.

Если посмотреть compareTo, в вызовы которого интерпретируется код на Kotlin при использовании знаков сравнения, то окажется что для LocalDateTime в принципе получается корректный код (сравниваются дни, часы, месяца и прочее по отдельности), что вроде как нормально.

В случае с OffsetDateTime будет сравнение, не которое мы ожидаем получить, так как compareTo учитывает зону времени, т.е. при сравнении 2021-04-25 10:00+0 и 2021-04-25 11:00+1 они не будут эквиваленты. Простой пример:

val inUtc = OffsetDateTime.of(LocalDateTime.of(2021, 4, 25, 10, 0), ZoneOffset.UTC)
val inUtc1 = OffsetDateTime.of(LocalDateTime.of(2021, 4, 25, 11, 0), ZoneOffset.ofTotalSeconds(60*60))
println(inUtc1>=inUtc && inUtc1 <= inUtc)
println(inUtc.isEqual(inUtc1))

Конечно корректно тут использовать не isEqual, а - == и вообще разделить isEqual+isBefore+isAfter и Comparable + equal, но к сожалению я склонен иногда не замечать разницы, особенно когда у меня есть удобный подход с операторами сравнения.

Вот так можно легко и непринужденно сотворить неочевидную багу в случае бездумного использования операторов сравнения в Котлтин с java.time api.

Similar posts

Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 6

    0

    Я не понял при чем тут зона времени. Такое поведение происходит из-за того, что сначала сравнение идет по Instant (и тут эти два офсета равны), а затем по LocalDateTime которые лежат в основе (и тут compareTo выдает "неожиданный" результат. Это сделано для консистентности с методом equals которую обязан гарантировать compareTo. Если ваш пример поменять на


    val inUtc = OffsetDateTime.of(LocalDateTime.of(2021, 4, 25, 10, 0), ZoneOffset.UTC)
    val inUtc1 = OffsetDateTime.of(LocalDateTime.of(2021, 4, 25, 11, 0), ZoneOffset.ofTotalSeconds(60*60))
    println(inUtc1>=inUtc && inUtc1 <= inUtc)
    println(inUtc == inUtc1)

    то оба принта выведут false.


    Но то что, equals так работает на OffsetDateTime это, конечно, очень неожиданно.

      0
      Спасибо за исправление. Согласен с Вами.
      Единственный момент: во время вызова compareTo первым идет вызов compareInstant в котором первой строкой сравнивается offset, что является частью таймзоны.
        0
        Касательно перемешивания мух и котлет (isEqual и compare) согласен с Вами. Суть «неожиданности» тут была не в проблемах OffsetDateTime, а неожиданность от использования  comparable с  LocalDateTime и OffsetDateTime, т.е. этакий ССЗБ получается во время перехода. Ровно такая же картина и с другими типа данных может случится.
        0
        Спасибо за коментарии
          +1
          Про Java версию — можно заменить два сравнения одним
           first.from.isEqual(second.to) || first.from.isBefore(second.to) 
          превращается в
          !first.from.isAfter(second.to)

          Тоже самое можно и со вторым вариантом проделать.
          Тогда возможно и не придется прибегать к сравнению по compareTo
            0

            Согласен. Спасибо

          Only users with full accounts can post comments. Log in, please.