aboutsummaryrefslogtreecommitdiff
path: root/rfc3339/date.go
blob: 4f11d033672f4fa14a56301c635f38398d70c2b5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
package rfc3339

import (
  "encoding/json"
  "fmt"
  "regexp"
  "strconv"
)

// RFC3339 date.
type Date uint16

// Returns true if the year is a leap year, and false otherwise.
//
// Reference:
// https://www.timeanddate.com/date/leapyear.html
func isLeapYear(y uint16) bool {
  // leap year rules:
  //   1. year is evenly divisible by 4 and is not evenly divisible by 100
  //   2. year is evenly divisible by 4 and is evenly divisible by 400
  return (((y % 4) == 0) && ((y % 100) != 0)) || // rule 1
         (((y % 4) == 0) && ((y % 400) == 0)) // rule 2
}

// maximum month days
var maxMonthDays = []uint16 {
  31, // jan
  28, // feb (incorrect for leap years!)
  31, // mar
  30, // apr
  31, // may
  30, // jun
  31, // jul
  31, // aug
  30, // sep
  31, // oct
  30, // nov
  31, // dec
}

// Get the maximum day for the given year and month.
func getMaxMonthDay(y, m uint16) uint16 {
  if m == 2 && isLeapYear(y) {
    return 29
  } else {
    return maxMonthDays[m - 1]
  }
}

// Parse date component and check range.
func parseDateComponent(name string, s []byte, min, max uint16) (uint16, error) {
  // parse value
  vr, err := strconv.ParseUint(string(s), 10, 32)
  if err != nil {
    return 0, err
  }

  v := uint16(vr)

  // check range
  if v < min || v > max {
    return 0, fmt.Errorf("%s component out of range [%d, %d]: %d", name, v, min, max)
  }

  return uint16(v), nil
}

var dateRe = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`)

// Create date from byte slice.
func NewDate(s []byte) (Date, error) {
  var r Date

  if !dateRe.Match(s) {
    return r, fmt.Errorf("invalid date: \"%s\"", s)
  }

  // parse year
  y, err := parseDateComponent("year", s[0:4], 1999, 2126)
  if err != nil {
    return r, err
  }

  // parse month
  m, err := parseDateComponent("month", s[5:7], 1, 12)
  if err != nil {
    return r, err
  }

  // parse day
  d, err := parseDateComponent("month", s[8:10], 1, getMaxMonthDay(y, m))
  if err != nil {
    return r, err
  }

  // encode return date
  r = Date((((y - 1999) & 0x3f) << 9) |
           (((m - 1) & 0xf) << 5) |
           ((d - 1) & 0x1f))

  // return result
  return r, nil
}

// Get year, month, and day components.
func (me Date) GetComponents() (uint16, uint16, uint16) {
  // extract components
  y := uint16((uint16(me) >> 9) + 1999)
  m := uint16(((uint16(me) >> 5) & 0xf) + 1)
  d := uint16((uint16(me) & 0x1f) + 1)

  return y, m, d
}

// Get year component.
func (me Date) Year() uint16 {
  y, _, _ := me.GetComponents()
  return y
}

// Get month component.
func (me Date) Month() uint16 {
  _, m, _ := me.GetComponents()
  return m
}

// Get day component.
func (me Date) Day() uint16 {
  _, _, d := me.GetComponents()
  return d
}

// Convert to string.
func (me Date) String() string {
  // extract date components
  y, m, d := me.GetComponents()

  // return string
  return fmt.Sprintf("%04d-%02d-%02d", y, m, d)
}

// Unmarshal date from JSON string.
func (me *Date) UnmarshalJSON(b []byte) error {
  // decode json string
  var s string
  if err := json.Unmarshal(b, &s); err != nil {
    return err
  }

  // create date
  d, err := NewDate([]byte(s))
  if err != nil {
    return err
  }

  // save result, return success
  *me = d
  return nil
}

// Marshal Date as JSON string.
func (d Date) MarshalJSON() ([]byte, error) {
  return json.Marshal(d.String())
}