Думаю, многие согласятся, что работа с датами практически в любом языке программирования это невероятная головная боль. Дата и время не десятичны, часовые пояса — господи, почему они не могут быть просто целыми — ну и конечно бесчисленное множество форматов даты и времени. Можно, конечно, возразить, что мол есть ISO 8601 и чего тебе дураку ещё надо, но давайте, положа руку на сердце, скажем — как часто вам доводилось сталкиваться с соблюдением этого стандарта в сторонних API? Не знаю, как с этим обстоят дела за рубежом, надеюсь, в комментариях мне подскажут, но на постсоветских просторах ситуация — обнять и плакать. Каждый использует свой собственный, только ему удобный, формат времени и даты и разбирайся с этим как хочешь.
Я расскажу про собственный опыт и про найденное решение.
Проблема, с которой я столкнулся, не выглядела так уж и ужасно, формат даты в API, с которым мне предстояло взимодействовать, выглядел так: YYYY-MM-DD hh:mm:ss. Смотрится вроде нормально, не так ли?
Нет.
Абсолютно ненормально.
Те, кто знаком с форматом ISO 8601, уже поняли в чем подвох. Для наглядности:
package main
import (
"encoding/json"
"log"
"time"
)
type Dated struct {
DateTime time.Time
}
func main() {
input := []byte("{\"datetime\": \"1900-01-01 12:00:04\"}")
var d Dated
err := json.Unmarshal(input, &d)
if err != nil {
log.Fatal(err)
}
}
В результате что? Правильно, ошибка:
parsing time ""1900-01-01 12:00:04"" as ""2006-01-02T15:04:05Z07:00"": cannot parse " 12:00:04"" as "T"
Что логично, ведь в Go из коробки есть ограниченное количество форматов времени и даты,
которые он может распарсить:
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"
Отсюда: https://golang.org/src/time/format.go
Как вы можете заметить, формата YYYY-MM-DD hh:mm:ss
там нет. Окей, беда понятна, но что делать то?
На самом деле проблема решается достаточно несложно, достаточно вспомнить, как в Go работает json.Unmarshall
. А работает он просто — у каждого поля структуры, в которую мы пытаемся смапить наш JSON, он ищет соответствие в нашем наборе байт и, если находит, смотрит на тип поля структуры и вызывает у этого типа функцию UnmasrhallJSON, в которую
передаёт найденное значение.
Соответственно, чтобы нам распарсить нестандартную дату, надо задать для поля кастомный
тип, который будет реализовывать интерфейс Unmarshaler, то бишь иметь собственную функцию UnmasrhallJSON:
type CustomDate struct {
time.Time
}
func (c *CustomDate) UnmarshalJSON(b []byte) (err error) {
layout := "2006-01-02 15:04:05"
s := strings.Trim(string(b), "\"") // remove quotes
if s == "null" {
return
}
c.Time, err = time.Parse(layout, s)
return
}
type Dated struct {
DateTime CustomDate
}
Ну и чтоб два раза не вставать, давайте добавим кастомный маршалер, нам ведь рано или поздно придется грузить данные в ту API, откуда мы их взяли, а значит, надо их снова привести к их нестандартному формату. В итоге всё вместе будет выглядеть так:
package main
import (
"encoding/json"
"fmt"
"log"
"strings"
"time"
)
type CustomDate struct {
time.Time
}
const layout = "2006-01-02 15:04:05"
func (c *CustomDate) UnmarshalJSON(b []byte) (err error) {
s := strings.Trim(string(b), `"`) // remove quotes
if s == "null" {
return
}
c.Time, err = time.Parse(layout, s)
return
}
func (c CustomDate) MarshalJSON() ([]byte, error) {
if c.Time.IsZero() {
return nil, nil
}
return []byte(fmt.Sprintf(`"%s"`, c.Time.Format(layout))), nil
}
type Dated struct {
DateTime CustomDate
}
func main() {
input := []byte("{\"datetime\": \"1900-01-01 12:00:04\"}")
var d Dated
err := json.Unmarshal(input, &d)
if err != nil {
log.Fatal(err)
}
log.Println("Unmarshal:")
log.Println(d.DateTime)
b, err := json.Marshal(d)
if err != nil {
log.Fatal(err)
}
log.Println("Marshal:")
log.Println(string(b))
}
Обратите внимание на сигнатуру функций Marshal/Unmarshal. Они разные — MarchalJSON принимает значение, а UnmarshalJSON — ссылку
Результат:
Unmarshal:
1900-01-01 12:00:04 +0000 UTC
Marshal:
{"DateTime":"1900-01-01T12:00:04Z"}
Вот, собственно, и всё.
Резюмируя
Парсинг нестарндартных дат в Go — дело нетривиальное, но несложное, достаточно написать собственный маршаллер/анмаршаллер, в зависимости от того, какую цель вы преследуете; также этот подход поможет справиться не только с датами — так можно успешно парсить любой нестандартный контент.
Если вы готовы предложить другие интересные способы решения этой проблемы — буду рад увидеть их в комментариях. Ну и в заключение хочу пожелать вам пореже сталкиваться с нестандартными форматами чего бы то ни было.
Всем спасибо.
sshikov
>часовые пояса — господи, почему они не могут быть просто целыми
В каком смысле целыми? Вы что, предлагаете закодировать Europe/Moscow в виде числа? И все остальные — тоже?
Papazian Автор
Я имел в виду, что часовые пояса могут быть сдвинуты как на целое количество часов, так и на дробное, например в Иране сдвиг UTC+3:30
sshikov
Я тогда не понял, что именно вы предлагаете? То что они не целые — это фича. Так устроено время в Иране. Можно конечно в минутах выразить… Но в целом приходится мириться с тем, что мир в этом месте устроен непросто, и нам с этим жить.
Papazian Автор
Это был просто плач Ярославны по поводу несовершенства мира, ничего я не предлагаю
sshikov
А-а-а) Ну в этом смысле могу только согласиться — с датами и временем все сложно. Я бы сказал — намного сложнее, чем вы в этом посте описали. Берем ту же зону Europe/Moscow, например. Что означает некий момент времени в этой зоне, если это было 10 лет назад? А хз, это нужно смотреть, какое описание зоны было тогда. А может какое сейчас? А, блин, наверное от задачи зависит… А если это была дата перехода на летнее время — то все становится еще чудесатее.