Разбор времени из различных форматов, не связанных с временными метками


Введение

В предыдущей статье мы реализовали эффективное решение для разбора временных меток в различных форматах временных меток (секунды, миллисекунды, микросекунды, наносекунды).

Как я уже упоминал в предыдущей статье, мы можем получить от клиента дату-время в строковом формате, в то время как мы ожидали строковую временную метку в секундах.

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

Давайте реализуем решение!

Я буду использовать Golang для всех примеров кода, но вы можете использовать любые другие языки, основываясь на предоставленном алгоритме.

Обзор проблемы

Прежде чем мы начнем писать код, нам необходимо понять суть проблемы.

Ожидания

Чтобы упростить текущую задачу, предположим, что у нас есть строковое поле, и нам нужно написать функцию, которая будет поддерживать несколько форматов времени.

Вот наши ожидания:

  1. У нас есть строковое поле, в котором мы ожидаем получить информацию RFC339 о дате и времени.
  2. Мы ожидаем увидеть временную метку в диапазоне дат и времени между текущее время - 3 дня и текущее время + 3 дня.
  3. Мы используем язык со строгой проверкой типов. Мы принимаем ТОЛЬКО строковое значение в нашем поле JSON.

Идентификация проблемы

Что мы неожиданно видим в наших журналах:

  • В журнале мы обнаружили ошибку типа 0001-01-01 00:00:00 +0000 UTC parsing time "Sat, 07 May 2022 19:22:10 PDT" as "2006-01-02 15:04:05": cannot parse "Sat, 07 May 2022 19:22:10 PDT" as "2006".
  • А также мы обнаружили ошибки такого типа 0001-01-01 00:00:00 +0000 UTC parsing time "05/07 07:22:54PM '22 -0700" as "2006-01-02 15:04:05": cannot parse "7 07:22:54PM '22 -0700" as "2006".

Возможные неправильные форматы

После некоторых исследований мы обнаружили следующие форматы, используемые клиентами:

Это означает, что некоторые наши клиенты используют неправильные стандартизированные форматы, вместо ожидаемых RFC3339.
Наряду с выявленными форматами мы предположили, что клиенты могут использовать еще несколько различных форматов, и нам необходимо связаться со всеми нашими клиентскими командами или партнерами, чтобы определить все возможные форматы.

Этапы решения проблемы

Исходя из нашего исследования, чтобы устранить проблему для большинства будущих случаев, нам необходимо выполнить следующие действия:

  • Поддерживать наиболее популярные форматы.
  • Разработать расширяемый API для поддержки дополнительных форматов.
  • Постарайтесь не переусложнять решение, для этой статьи.

Известные ограничения

  • Конечно, эта функция не будет поддерживать все возможные форматы, поэтому при необходимости мы должны будем расширить ее на дополнительные форматы.
  • Мы будем использовать только форматы из предопределенного списка в стандартной библиотеке Golang.
  • Возможны конфликты между различными форматами. Порядок предоставляемых форматов имеет значение.

Решение

Прежде чем приступить к написанию кода, нам необходимо провести некоторую подготовку.

  1. Понять, как разобрать дату и время в Golang.
  2. Найдите список поддерживаемых форматов в стандартной библиотеке Golang.
  3. Реализуйте функцию.

Как разобрать дату и время в Golang

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

Чтобы разобрать дату и время, нам нужно использовать функцию time.Parse.

package main

import (
  "fmt"
  "time"
)

func main() {
  now := time.Now()                                                 // Get current date time.
  parsed, err := time.Parse(time.RFC3339, now.Format(time.RFC3339)) // Parse current date time in RFC3339 format.
  fmt.Println(parsed, err)                                          // 2022-05-07 20:07:11 -0700 PDT <nil>
}
Вход в полноэкранный режим Выйти из полноэкранного режима

В примере выше мы анализируем текущую дату-время в формате RFC3339, который содержит макет 2006-01-02T15:04:05Z07:00.
Как мы видим, в golang мы используем конкретную дату времени в качестве макета, чтобы указать формат, который мы будем разбирать.
Это необычно, но поскольку мы не собираемся писать собственные форматы, задача не потребует детального знания этих внутренних реализаций.

Как мы видим, есть два различных параметра, возвращаемых из функции time.Parse: time и error.
Если error равен nil, то мы успешно разобрали дату-время, в противном случае что-то пошло не так.

Рассмотрим пример с различными форматами:

package main

import (
  "fmt"
  "time"
)

func main() {
  now := time.Now()                                                 // Get current date time.
  parsed, err := time.Parse(time.RFC3339, now.Format(time.RFC1123)) // Parse current date time in RFC3339 format.
  fmt.Println(parsed, err)                                          // 0001-01-01 00:00:00 +0000 UTC parsing time "Sat, 07 May 2022 20:09:12 PDT" as "2006-01-02T15:04:05Z07:00": cannot parse "Sat, 07 May 2022 20:09:12 PDT" as "2006".
}
Вход в полноэкранный режим Выход из полноэкранного режима

Как видно из вывода, мы получили пустую дату-время 0001-01-01 00:00:00 +0000 UTC и ошибку parsing time "Sat, 07 May 2022 20:09:12 PDT" as "2006-01-02T15:04:05Z07:00": cannot parse "Sat, 07 May 2022 20:09:12 PDT" as "2006" about wrong format.

Мы будем использовать это поведение для разбора даты-времени в различных форматах.

Список поддерживаемых форматов

Как я уже говорил, нам необходимо поддерживать все форматы из стандартной библиотеки Golang.

Все поддерживаемые форматы перечислены в пакете time стандартной библиотеки. Мы можем найти его в документации.

Я перечислю все поддерживаемые форматы в текущей последней версии Golang:

package time

const (
  Layout      = "01/02 03:04:05PM '06 -0700" // The reference time, in numerical order.
  ANSIC       = "Mon Jan _2 15:04:05 2006"
  UnixDate    = "Mon Jan _2 15:04:05 MST 2006"
  RubyDate    = "Mon Jan 02 15:04:05 -0700 2006"
  RFC822      = "02 Jan 06 15:04 MST"
  RFC822Z     = "02 Jan 06 15:04 -0700" // RFC822 with numeric zone
  RFC850      = "Monday, 02-Jan-06 15:04:05 MST"
  RFC1123     = "Mon, 02 Jan 2006 15:04:05 MST"
  RFC1123Z    = "Mon, 02 Jan 2006 15:04:05 -0700" // RFC1123 with numeric zone
  RFC3339     = "2006-01-02T15:04:05Z07:00"
  RFC3339Nano = "2006-01-02T15:04:05.999999999Z07:00"
  Kitchen     = "3:04PM"
  // Handy time stamps.
  Stamp      = "Jan _2 15:04:05"
  StampMilli = "Jan _2 15:04:05.000"
  StampMicro = "Jan _2 15:04:05.000000"
  StampNano  = "Jan _2 15:04:05.000000000"
)
Войти в полноэкранный режим Выйти из полноэкранного режима

Эффективное решение

Для реализации мы будем использовать максимально простой алгоритм:

  1. Итерация по всем поддерживаемым форматам.
  2. Разберите полученную строку с текущим форматом.
  3. Если возвращаемая ошибка равна nil, значит, мы нашли правильный формат. Мы можем вернуться из функции.
  4. В противном случае необходимо продолжить итерацию.

Промежуточное решение:

package main

import (
  "fmt"
  "time"
)

func main() {
  t := time.Date(2022, time.May, 23, 7, 3, 5, 734423, time.UTC)

  fmt.Println(parseTime(t.Format(time.RFC3339))) // 2022-05-23 07:03:05 +0000 UTC <nil>
  fmt.Println(parseTime(t.Format(time.RFC1123))) // 2022-05-23 07:03:05 +0000 UTC <nil>
  fmt.Println(parseTime(t.Format(time.Layout)))  // 0001-01-01 00:00:00 +0000 UTC could not parse time: 05/23 07:03:05AM '22 +0000
}

func parseTime(dt string) (time.Time, error) {
  var formats = []string{
    time.RFC3339,
    time.RFC1123,
  }

  for _, format := range formats {
    parsedTime, err := time.Parse(format, dt)
    if err == nil {
      return parsedTime, nil
    }
  }

  return time.Time{}, fmt.Errorf("could not parse time: %s", dt)
}
Войти в полноэкранный режим Выход из полноэкранного режима

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

Есть ограничение, которое мы можем распознать на основе приведенного выше примера: мы не можем последовательно разобрать форматы, которые являются подформатами следующего формата.
Таким образом, нам нужно быть уверенными, что мы предоставляем временные раскладки в форматах списка от самого специфического до самого общего.

Реализуем функцию, которая будет покрывать все требования, которые мы уже указали:

package main

import (
  "fmt"
  "time"
)

func main() {
  // Specify all formats in the specific order.
  formats := []string{
    time.RFC3339Nano,
    time.RFC3339,
    time.RFC1123Z,
    time.RFC1123,
    time.RFC850,
    time.RFC822Z,
    time.RFC822,
    time.Layout,
    time.RubyDate,
    time.UnixDate,
    time.ANSIC,
    time.StampNano,
    time.StampMicro,
    time.StampMilli,
    time.Stamp,
    time.Kitchen,
  }

  t := time.Date(2022, time.May, 23, 7, 3, 5, 234734423, time.UTC)

  fmt.Println(parseTime(formats, t.Format(time.RFC3339Nano))) // 2022-05-23 07:03:05.234734423 +0000 UTC <nil>
  fmt.Println(parseTime(formats, t.Format(time.RFC3339)))     // 2022-05-23 07:03:05 +0000 UTC <nil>
  fmt.Println(parseTime(formats, t.Format(time.RFC1123Z)))    // 2022-05-23 07:03:05 +0000 +0000 <nil>
  fmt.Println(parseTime(formats, t.Format(time.RFC1123)))     // 2022-05-23 07:03:05 +0000 UTC <nil>
  fmt.Println(parseTime(formats, t.Format(time.RFC850)))      // 2022-05-23 07:03:05 +0000 UTC <nil>
  fmt.Println(parseTime(formats, t.Format(time.RFC822Z)))     // 2022-05-23 07:03:00 +0000 +0000 <nil>
  fmt.Println(parseTime(formats, t.Format(time.RFC822)))      // 2022-05-23 07:03:00 +0000 UTC <nil>
  fmt.Println(parseTime(formats, t.Format(time.Layout)))      // 2022-05-23 07:03:05 +0000 +0000 <nil>
  fmt.Println(parseTime(formats, t.Format(time.RubyDate)))    // 2022-05-23 07:03:05 +0000 +0000 <nil>
  fmt.Println(parseTime(formats, t.Format(time.UnixDate)))    // 2022-05-23 07:03:00 +0000 UTC <nil>
  fmt.Println(parseTime(formats, t.Format(time.ANSIC)))       // 2022-05-23 07:03:00 +0000 UTC <nil>
  fmt.Println(parseTime(formats, t.Format(time.StampNano)))   // 2022-05-23 07:03:05.234734423 +0000 UTC <nil>
  fmt.Println(parseTime(formats, t.Format(time.StampMicro)))  // 0000-05-23 07:03:05.234734 +0000 UTC <nil>
  fmt.Println(parseTime(formats, t.Format(time.StampMilli)))  // 0000-05-23 07:03:05.234 +0000 UTC <nil>
  fmt.Println(parseTime(formats, t.Format(time.Stamp)))       // 0000-05-23 07:03:05 +0000 UTC <nil>
  fmt.Println(parseTime(formats, t.Format(time.Kitchen)))     // 0000-01-01 07:03:00 +0000 UTC <nil>
}

func parseTime(formats []string, dt string) (time.Time, error) {
  for _, format := range formats {
    parsedTime, err := time.Parse(format, dt)
    if err == nil {
      return parsedTime, nil
    }
  }

  return time.Time{}, fmt.Errorf("could not parse time: %s", dt)
}
Войти в полноэкранный режим Выход из полноэкранного режима

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

Похоже, что все форматы были разобраны без каких-либо проблем, и мы будем считать, что этот алгоритм корректен и пригоден для нашего приложения.

Заключение

В этой статье мы решили еще одну проблему, связанную с разбором строки дата-время.
Теперь мы можем считать, что наши клиенты защищены от всех предопределенных форматов.

Текущая реализация может быть использована без каких-либо ограничений во всех ваших проектах. Но, обратите внимание, что вы охватили все необходимые форматы для вашего бизнеса. Возможно, вам потребуется добавить в список еще несколько форматов.

Надеюсь, эта статья поможет вам сэкономить время.

Оцените статью
Procodings.ru
Добавить комментарий