세계시 UnmarshalJSON 구현

30305 단어 gotime

소개



서로 다른 날짜 시간 형식에 대한 파서를 작성하여 두 가지 복잡한 단계를 수행했습니다.
  • 타임스탬프 형식의 시간입니다.
  • RFC3339와 같은 타임스탬프 형식이 아닌 형식의 시간입니다.

  • 다음이자 마지막 단계는 실제 및 실제 문제를 해결하는 실제 함수를 작성하는 것입니다.
    데이터 유형이 다른 다양한 형식의 JSON 필드를 구문 분석해야 합니다.

    실제 코드를 작성하기 전에 몇 가지 조사 단계를 거치겠습니다.

    I'll use Golang for all code examples, but you can use any other languages base on the provided algorithm.


    작업 개요



    현재 작업은 JSON 구문 분석 파이프라인에서 사용할 수 있는 Golang 유형을 작성하는 것입니다.
    이 필드는 이전 기사에서 이미 검토한 몇 가지 사용 사례를 지원해야 합니다.

    다음과 같은 JSON 필드를 받았습니다.
  • 숫자 유형이며 타임스탬프로 구문 분석해야 합니다.
  • 문자열 유형이며 타임스탬프로 구문 분석해야 합니다.
  • 문자열 유형이며 제공된 목록에서 일부 레이아웃으로 구문 분석할 것으로 예상됩니다.

  • 기대



    위의 사용 사례를 기반으로 다음과 같은 JSON 필드 목록이 표시될 것으로 예상됩니다.

    {
      "num_seconds": 1651808102,
      "num_milliseconds": 1651808102363,
      "num_microseconds": 1651808102363368,
      "num_nanoseconds": 1651808102363368423,
    
      "hex_seconds": "0x62749766",
    
      "str_seconds": "1651808102",
      "str_milliseconds": "1651808102363",
      "str_microseconds": "1651808102363368",
      "str_nanoseconds": "1651808102363368423",
    
      "str_rfc3339": "2022-05-06T03:35:02Z",
      "str_rfc3339_nano": "2022-05-06T03:35:02.363368423Z",
      "str_rfc1123": "Fri, 06 May 2022 03:35:02 UTC",
      "str_rfc850": "Friday, 06-May-22 03:35:02 UTC",
      "str_rfc822": "06 May 22 03:35 UTC"
    }
    


    분명히 전체 유형 목록은 아니지만 이전에 포괄적인 사례 목록을 테스트했으므로 지금 바로 단순화할 수 있습니다.
    strconv.ParseInt 함수의 마법 때문에 작동하는지 확인하기 위해 16진수 초 필드를 추가했습니다.

    UnmarshalJSON 인터페이스 개요



    구현하기 전에 사용자 정의 JSON 데이터 유형 구현의 Golang 방식을 고려해야 합니다.

    Golang에는 하나의 메서드UnmarshalJSON([]byte) error가 있는 인터페이스Unmarshaler가 포함되어 있습니다.
    이는 JSON 필드를 구문 분석하기 위해 하나의 메서드만 구현해야 함을 의미합니다.

    JSON 필드에서 밑줄_로 구분된 값이 있는 문자열을 2개의 변수로 구분하기만 하면 되는 간단한 인터페이스 구현의 예가 있습니다.

    package main
    
    import (
      "encoding/json"
      "errors"
      "strings"
    )
    
    type unmarshaled struct {
      part1 string
      part2 string
    }
    
    func (u *unmarshaled) UnmarshalJSON(text []byte) error {
      str := string(text)
      parts := strings.Split(str, "_")
      if len(parts) != 2 {
        return errors.New("invalid format")
      }
    
      u.part1 = parts[0]
      u.part2 = parts[1]
    
      return nil
    }
    
    type jsonType struct {
      T unmarshaled `json:"t"`
    }
    
    func main() {
      var jt jsonType
    
      json1 := `{"t": "one_two"}`
    
      err := json.Unmarshal([]byte(json1), &jt)
      if err != nil {
        panic(err)
      }
    
      println(jt.T.part1, jt.T.part2)
    
      json2 := `{"t": "onetwo"}`
    
      err = json.Unmarshal([]byte(json2), &jt)
      if err != nil {
        panic(err)
      }
    
      println(jt.T.part1, jt.T.part2)
    }
    


    산출:

    "one two"
    panic: invalid format
    
    goroutine 1 [running]:
    main.main()
            /Users/aohorodnyk/projects/anton.ohorodnyk.name/main.go:47 +0x13c
    exit status 2
    


    첫 번째 입력은 올바르게 구문 분석되었지만 두 번째 입력은 실패했습니다.

    구현



    최종 구현에는 다음 단계가 포함됩니다.
  • 가장 빠른 조건이므로 JSON 필드가 숫자인지 확인하십시오.
  • 숫자인 경우 타임스탬프로 구문 분석할 수 있습니다.
  • strconv.ParseInt 함수를 사용하여 구문 분석합니다.
  • strconv.ParseInt 함수는 문자열의 접두사를 기반으로 유형을 구문 분석합니다. 접두사가 0x 인 경우 16진수로 구문 분석됩니다. 0b는 이진수 등입니다. 기본적으로 십진수로 구문 분석됩니다.
  • 숫자 값을 정수로 처리할 수 없는 경우 값과 관련이 있는지 모르기 때문에 오류를 반환합니다.
  • 그렇지 않으면 시간을 설정하고 처리를 완료합니다.

  • 그렇지 않으면 값이 문자열입니다. 문자열의 경우 값을 일반 문자열로 만들기 위해 따옴표를 잘라야 합니다.
  • 지원되는 모든 레이아웃으로 문자열을 구문 분석해 보십시오.
  • 나열된 일부 레이아웃으로 문자열을 성공적으로 구문 분석하면 시간을 설정하고 처리를 완료합니다.
  • 필요한 레이아웃을 찾지 못한 경우 문자열을 타임스탬프로 구문 분석해 보십시오.
  • 문자열을 타임스탬프로 구문 분석하는 데 성공하면 시간을 설정하고 처리를 완료합니다.
  • 그렇지 않으면 오류를 반환합니다.

  • 다음 코드 예제는 위에서 설명한 알고리즘을 구현합니다.

    package main
    
    import (
      "encoding/json"
      "errors"
      "fmt"
      "math"
      "strconv"
      "strings"
      "time"
    )
    
    // List of supported time layouts.
    var 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,
    }
    
    const (
      maxNanoseconds  = int64(math.MaxInt64)
      maxMicroseconds = int64(maxNanoseconds / 1000)
      maxMilliseconds = int64(maxMicroseconds / 1000)
      maxSeconds      = int64(maxMilliseconds / 1000)
    
      minNanoseconds  = int64(math.MinInt64)
      minMicroseconds = int64(minNanoseconds / 1000)
      minMilliseconds = int64(minMicroseconds / 1000)
      minSeconds      = int64(minMilliseconds / 1000)
    )
    
    type InternalTime struct {
      time.Time
    }
    
    func (it *InternalTime) UnmarshalJSON(data []byte) error {
      // Make sure that the input is not empty
      if len(data) == 0 {
        return errors.New("empty value is not supported")
      }
    
      // If the input is not a string, try to parse it as a number, otherwise return an error.
      if data[0] != '"' {
        timeInt64, err := strconv.ParseInt(string(data), 0, 64)
        if err != nil {
          return err
        }
    
        it.Time = parseTimestamp(timeInt64)
      }
    
      // If the input is a string, trim quotes.
      str := strings.Trim(string(data), `"`)
    
      // Parse the string as a time using the supported layouts.
      parsed, err := parseTime(formats, str)
      if err == nil {
        it.Time = parsed
    
        return nil
      }
    
      // As the final attempt, try to parse the string as a timestamp.
      timeInt64, err := strconv.ParseInt(str, 0, 64)
      if err == nil {
        it.Time = parseTimestamp(timeInt64)
    
        return nil
      }
    
      return errors.New("Unsupported time format")
    }
    
    type jsonType struct {
      NumSeconds      InternalTime `json:"num_seconds"`
      NumMilliseconds InternalTime `json:"num_milliseconds"`
      NumMicroseconds InternalTime `json:"num_microseconds"`
      NumNanoseconds  InternalTime `json:"num_nanoseconds"`
    
      HexSeconds InternalTime `json:"hex_seconds"`
    
      StrSeconds      InternalTime `json:"str_seconds"`
      StrMilliseconds InternalTime `json:"str_milliseconds"`
      StrMicroseconds InternalTime `json:"str_microseconds"`
      StrNanoseconds  InternalTime `json:"str_nanoseconds"`
    
      StrRFC3339     InternalTime `json:"str_rfc3339"`
      StrRFC3339Nano InternalTime `json:"str_rfc3339_nano"`
      StrRFC1123     InternalTime `json:"str_rfc1123"`
      StrRFC850      InternalTime `json:"str_rfc850"`
      StrRFC822      InternalTime `json:"str_rfc822"`
    }
    
    func main() {
      var jt jsonType
    
      json1 := `{
        "num_seconds": 1651808102,
        "num_milliseconds": 1651808102363,
        "num_microseconds": 1651808102363368,
        "num_nanoseconds": 1651808102363368423,
    
        "hex_seconds": "0x62749766",
    
        "str_seconds": "1651808102",
        "str_milliseconds": "1651808102363",
        "str_microseconds": "1651808102363368",
        "str_nanoseconds": "1651808102363368423",
    
        "str_rfc3339": "2022-05-06T03:35:02Z",
        "str_rfc3339_nano": "2022-05-06T03:35:02.363368423Z",
        "str_rfc1123": "Fri, 06 May 2022 03:35:02 UTC",
        "str_rfc850": "Friday, 06-May-22 03:35:02 UTC",
        "str_rfc822": "06 May 22 03:35 UTC"
      }`
    
      err := json.Unmarshal([]byte(json1), &jt)
      if err != nil {
        panic(err)
      }
    
      fmt.Println(jt) // {2022-05-05 20:35:02 -0700 PDT 2022-05-05 20:35:02.363 -0700 PDT 2022-05-05 20:35:02.363368 -0700 PDT 2022-05-05 20:35:02.363368423 -0700 PDT 2022-05-05 20:35:02 -0700 PDT 2022-05-05 20:35:02 -0700 PDT 2022-05-05 20:35:02.363 -0700 PDT 2022-05-05 20:35:02.363368 -0700 PDT 2022-05-05 20:35:02.363368423 -0700 PDT 2022-05-06 03:35:02 +0000 UTC 2022-05-06 03:35:02.363368423 +0000 UTC 2022-05-06 03:35:02 +0000 UTC 2022-05-06 03:35:02 +0000 UTC 2022-05-06 03:35:00 +0000 UTC}
    }
    
    func parseTimestamp(timestamp int64) time.Time {
      switch {
      case timestamp < minMicroseconds:
        return time.Unix(0, timestamp) // Before 1970 in nanoseconds.
      case timestamp < minMilliseconds:
        return time.Unix(0, timestamp*int64(time.Microsecond)) // Before 1970 in microseconds.
      case timestamp < minSeconds:
        return time.Unix(0, timestamp*int64(time.Millisecond)) // Before 1970 in milliseconds.
      case timestamp < 0:
        return time.Unix(timestamp, 0) // Before 1970 in seconds.
      case timestamp < maxSeconds:
        return time.Unix(timestamp, 0) // After 1970 in seconds.
      case timestamp < maxMilliseconds:
        return time.Unix(0, timestamp*int64(time.Millisecond)) // After 1970 in milliseconds.
      case timestamp < maxMicroseconds:
        return time.Unix(0, timestamp*int64(time.Microsecond)) // After 1970 in microseconds.
      }
    
      return time.Unix(0, timestamp) // After 1970 in nanoseconds.
    }
    
    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)
    }
    


    위의 코드는 긍정적인 테스트 사례를 다루며 알고리즘이 문제 없이 작동하고 구현이 올바르다는 것을 증명합니다.

    결론



    최근 3개의 기사에서 우리는 클라이언트의 개발자 경험을 개선하는 데 사용할 수 있는 범용 시간 파서를 구현했습니다.
    생산 버그 및 오류의 양을 줄이는 데 도움이 될 수 있습니다.

    이 기사가 귀하와 귀하의 팀에 도움이 되기를 바랍니다.

    당신이 가질 수 있는 질문을 자유롭게 작성하십시오.

    좋은 웹페이지 즐겨찾기