Думаю, многие согласятся, что работа с датами практически в любом языке программирования это невероятная головная боль. Дата и время не десятичны, часовые пояса — господи, почему они не могут быть просто целыми — ну и конечно бесчисленное множество форматов даты и времени. Можно, конечно, возразить, что мол есть ISO 8601 и чего тебе дураку ещё надо, но давайте, положа руку на сердце, скажем — как часто вам доводилось сталкиваться с соблюдением этого стандарта в сторонних API? Не знаю, как с этим обстоят дела за рубежом, надеюсь, в комментариях мне подскажут, но на постсоветских просторах ситуация — обнять и плакать. Каждый использует свой собственный, только ему удобный, формат времени и даты и разбирайся с этим как хочешь.
Я расскажу про собственный опыт и про найденное решение.
Проблема, с которой я столкнулся, не выглядела так уж и ужасно, формат даты в API, с которым мне предстояло взимодействовать, выглядел так: YYYY-MM-DD hh:mm:dd. Смотрится вроде нормально, не так ли?
Нет.
Абсолютно ненормально.
Те, кто знаком с форматом 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 — дело нетривиальное, но несложное, достаточно написать собственный маршаллер/анмаршаллер, в зависимости от того, какую цель вы преследуете; также этот подход поможет справиться не только с датами — так можно успешно парсить любой нестандартный контент.
Если вы готовы предложить другие интересные способы решения этой проблемы — буду рад увидеть их в комментариях. Ну и в заключение хочу пожелать вам пореже сталкиваться с нестандартными форматами чего бы то ни было.
Всем спасибо.
ссылка на оригинал статьи https://habr.com/ru/post/492996/
Добавить комментарий