rfc3339.odin 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. package time
  2. // Parsing RFC 3339 date/time strings into time.Time.
  3. // See https://www.rfc-editor.org/rfc/rfc3339 for the definition
  4. import dt "core:time/datetime"
  5. /*
  6. Parse an RFC 3339 string into time with a UTC offset applied to it.
  7. This procedure parses the specified RFC 3339 strings of roughly the following
  8. format:
  9. ```text
  10. YYYY-MM-DD[Tt]HH:mm:ss[.nn][Zz][+-]HH:mm
  11. ```
  12. And returns the time that was represented by the RFC 3339 string, with the UTC
  13. offset applied to it.
  14. **Inputs**:
  15. - `rfc_datetime`: An RFC 3339 string to parse.
  16. - `is_leap`: Optional output parameter specifying whether the moment was a leap
  17. second.
  18. **Returns**:
  19. - `res`: The time, with UTC offset applied, that was parsed from the RFC 3339
  20. string.
  21. - `consumed`: The number of bytes consumed by parsing the RFC 3339 string.
  22. **Notes**:
  23. - Only 4-digit years are accepted.
  24. - Leap seconds are smeared into 23:59:59.
  25. */
  26. rfc3339_to_time_utc :: proc(rfc_datetime: string, is_leap: ^bool = nil) -> (res: Time, consumed: int) {
  27. offset: int
  28. res, offset, consumed = rfc3339_to_time_and_offset(rfc_datetime, is_leap)
  29. res._nsec += (i64(-offset) * i64(Minute))
  30. return res, consumed
  31. }
  32. /*
  33. Parse an RFC 3339 string into a time and a UTC offset in minutes.
  34. This procedure parses the specified RFC 3339 strings of roughly the following
  35. format:
  36. ```text
  37. YYYY-MM-DD[Tt]HH:mm:ss[.nn][Zz][+-]HH:mm
  38. ```
  39. And returns the time, in UTC and a UTC offset, in minutes, that were represented
  40. by the RFC 3339 string.
  41. **Inputs**:
  42. - `rfc_datetime`: The RFC 3339 string to be parsed.
  43. - `is_leap`: Optional output parameter specifying whether the moment was a
  44. leap second.
  45. **Returns**:
  46. - `res`: The time, in UTC, that was parsed from the RFC 3339 string.
  47. - `utc_offset`: The UTC offset, in minutes, that was parsed from the RFC 3339
  48. string.
  49. - `consumed`: The number of bytes consumed by parsing the string.
  50. **Notes**:
  51. - Only 4-digit years are accepted.
  52. - Leap seconds are smeared into 23:59:59.
  53. */
  54. rfc3339_to_time_and_offset :: proc(rfc_datetime: string, is_leap: ^bool = nil) -> (res: Time, utc_offset: int, consumed: int) {
  55. moment, offset, leap_second, count := rfc3339_to_components(rfc_datetime)
  56. if count == 0 {
  57. return
  58. }
  59. if is_leap != nil {
  60. is_leap^ = leap_second
  61. }
  62. if _res, ok := datetime_to_time(moment.year, moment.month, moment.day, moment.hour, moment.minute, moment.second, moment.nano); !ok {
  63. return {}, 0, 0
  64. } else {
  65. return _res, offset, count
  66. }
  67. }
  68. /*
  69. Parse an RFC 3339 string into a datetime and a UTC offset in minutes.
  70. This procedure parses the specified RFC 3339 strings of roughly the following
  71. format:
  72. ```text
  73. YYYY-MM-DD[Tt]HH:mm:ss[.nn][Zz][+-]HH:mm
  74. ```
  75. And returns the datetime, in UTC and the UTC offset, in minutes, that were
  76. represented by the RFC 3339 string.
  77. **Inputs**:
  78. - `rfc_datetime`: The RFC 3339 string to parse.
  79. **Returns**:
  80. - `res`: The datetime, in UTC, that was parsed from the RFC 3339 string.
  81. - `utc_offset`: The UTC offset, in minutes, that was parsed from the RFC 3339
  82. string.
  83. - `is_leap`: Specifies whether the moment was a leap second.
  84. - `consumed`: Number of bytes consumed by parsing the string.
  85. Performs no validation on whether components are valid, e.g. it'll return hour = 25 if that's what it's given
  86. */
  87. rfc3339_to_components :: proc(rfc_datetime: string) -> (res: dt.DateTime, utc_offset: int, is_leap: bool, consumed: int) {
  88. moment, offset, count, leap_second, ok := _rfc3339_to_components(rfc_datetime)
  89. if !ok {
  90. return
  91. }
  92. return moment, offset, leap_second, count
  93. }
  94. // Parses an RFC 3339 string and returns datetime.DateTime.
  95. // Performs no validation on whether components are valid, e.g. it'll return hour = 25 if that's what it's given
  96. @(private)
  97. _rfc3339_to_components :: proc(rfc_datetime: string) -> (res: dt.DateTime, utc_offset: int, consumed: int, is_leap: bool, ok: bool) {
  98. // A compliant date is at minimum 20 characters long, e.g. YYYY-MM-DDThh:mm:ssZ
  99. (len(rfc_datetime) >= 20) or_return
  100. // Scan and eat YYYY-MM-DD[Tt], then scan and eat HH:MM:SS, leave separator
  101. year := scan_digits(rfc_datetime[0:], "-", 4) or_return
  102. month := scan_digits(rfc_datetime[5:], "-", 2) or_return
  103. day := scan_digits(rfc_datetime[8:], "Tt ", 2) or_return
  104. hour := scan_digits(rfc_datetime[11:], ":", 2) or_return
  105. minute := scan_digits(rfc_datetime[14:], ":", 2) or_return
  106. second := scan_digits(rfc_datetime[17:], "", 2) or_return
  107. nanos := 0
  108. count := 19
  109. if rfc_datetime[count] == '.' {
  110. // Scan hundredths. The string must be at least 4 bytes long (.hhZ)
  111. (len(rfc_datetime[count:]) >= 4) or_return
  112. hundredths := scan_digits(rfc_datetime[count+1:], "", 2) or_return
  113. count += 3
  114. nanos = 10_000_000 * hundredths
  115. }
  116. // Leap second handling
  117. if minute == 59 && second == 60 {
  118. second = 59
  119. is_leap = true
  120. }
  121. err: dt.Error
  122. if res, err = dt.components_to_datetime(year, month, day, hour, minute, second, nanos); err != .None {
  123. return {}, 0, 0, false, false
  124. }
  125. // Scan UTC offset
  126. switch rfc_datetime[count] {
  127. case 'Z', 'z':
  128. utc_offset = 0
  129. count += 1
  130. case '+', '-':
  131. (len(rfc_datetime[count:]) >= 6) or_return
  132. offset_hour := scan_digits(rfc_datetime[count+1:], ":", 2) or_return
  133. offset_minute := scan_digits(rfc_datetime[count+4:], "", 2) or_return
  134. utc_offset = 60 * offset_hour + offset_minute
  135. utc_offset *= -1 if rfc_datetime[count] == '-' else 1
  136. count += 6
  137. }
  138. return res, utc_offset, count, is_leap, true
  139. }
  140. @(private)
  141. scan_digits :: proc(s: string, sep: string, count: int) -> (res: int, ok: bool) {
  142. needed := count + min(1, len(sep))
  143. (len(s) >= needed) or_return
  144. #no_bounds_check for i in 0..<count {
  145. if v := s[i]; v >= '0' && v <= '9' {
  146. res = res * 10 + int(v - '0')
  147. } else {
  148. return 0, false
  149. }
  150. }
  151. found_sep := len(sep) == 0
  152. #no_bounds_check for v in sep {
  153. found_sep |= rune(s[count]) == v
  154. }
  155. return res, found_sep
  156. }
  157. /*
  158. Serialize the timestamp as a RFC 3339 string.
  159. The boolean `ok` is false if the `time` is not a valid datetime, or if allocating the result string fails.
  160. **Inputs**:
  161. - `utc_offset`: offset in minutes wrt UTC (ie. the timezone)
  162. - `include_nanos`: whether to include nanoseconds in the result.
  163. */
  164. time_to_rfc3339 :: proc(time: Time, utc_offset : int = 0, include_nanos := true, allocator := context.allocator) -> (res: string, ok: bool) {
  165. utc_offset := utc_offset
  166. // convert to datetime
  167. datetime := time_to_datetime(time) or_return
  168. if datetime.year < 0 || datetime.year >= 10_000 { return "", false }
  169. temp_string := [36]u8{}
  170. offset : uint = 0
  171. print_as_fixed_int :: proc(dst: []u8, offset: ^uint, width: i8, i: i64) {
  172. i := i
  173. width := width
  174. for digit_idx in 0..<width {
  175. last_digit := i % 10
  176. dst[offset^ + uint(width) - uint(digit_idx)-1] = '0' + u8(last_digit)
  177. i = i / 10
  178. }
  179. offset^ += uint(width)
  180. }
  181. print_as_fixed_int(temp_string[:], &offset, 4, datetime.year)
  182. temp_string[offset] = '-'
  183. offset += 1
  184. print_as_fixed_int(temp_string[:], &offset, 2, i64(datetime.month))
  185. temp_string[offset] = '-'
  186. offset += 1
  187. print_as_fixed_int(temp_string[:], &offset, 2, i64(datetime.day))
  188. temp_string[offset] = 'T'
  189. offset += 1
  190. print_as_fixed_int(temp_string[:], &offset, 2, i64(datetime.hour))
  191. temp_string[offset] = ':'
  192. offset += 1
  193. print_as_fixed_int(temp_string[:], &offset, 2, i64(datetime.minute))
  194. temp_string[offset] = ':'
  195. offset += 1
  196. print_as_fixed_int(temp_string[:], &offset, 2, i64(datetime.second))
  197. // turn 123_450_000 to 12345, 5
  198. strip_trailing_zeroes_nanos :: proc(n: i64) -> (res: i64, n_digits: i8) {
  199. res = n
  200. n_digits = 9
  201. for res % 10 == 0 {
  202. res = res / 10
  203. n_digits -= 1
  204. }
  205. return
  206. }
  207. // pre-epoch times: turn, say, -400ms to +600ms for display
  208. nanos := time._nsec % 1_000_000_000
  209. if nanos < 0 {
  210. nanos += 1_000_000_000
  211. }
  212. if nanos != 0 && include_nanos {
  213. temp_string[offset] = '.'
  214. offset += 1
  215. // remove trailing zeroes
  216. nanos_nonzero, n_digits := strip_trailing_zeroes_nanos(nanos)
  217. assert(nanos_nonzero != 0)
  218. // write digits, right-to-left
  219. for digit_idx : i8 = n_digits-1; digit_idx >= 0; digit_idx -= 1 {
  220. digit := u8(nanos_nonzero % 10)
  221. temp_string[offset + uint(digit_idx)] = '0' + u8(digit)
  222. nanos_nonzero /= 10
  223. }
  224. offset += uint(n_digits)
  225. }
  226. if utc_offset == 0 {
  227. temp_string[offset] = 'Z'
  228. offset += 1
  229. } else {
  230. temp_string[offset] = utc_offset > 0 ? '+' : '-'
  231. offset += 1
  232. utc_offset = abs(utc_offset)
  233. print_as_fixed_int(temp_string[:], &offset, 2, i64(utc_offset / 60))
  234. temp_string[offset] = ':'
  235. offset += 1
  236. print_as_fixed_int(temp_string[:], &offset, 2, i64(utc_offset % 60))
  237. }
  238. res_as_slice, res_alloc := make_slice([]u8, len=offset, allocator = allocator)
  239. if res_alloc != nil {
  240. return "", false
  241. }
  242. copy(res_as_slice, temp_string[:offset])
  243. return string(res_as_slice), true
  244. }