aboutsummaryrefslogtreecommitdiff
path: root/core/time/rfc3339.odin
blob: 3e0bab9169952a6e5ce93ad198dceb0d5f26638e (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
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
package time
// Parsing RFC 3339 date/time strings into time.Time.
// See https://www.rfc-editor.org/rfc/rfc3339 for the definition

import dt "core:time/datetime"

/*
Parse an RFC 3339 string into time with a UTC offset applied to it.

This procedure parses the specified RFC 3339 strings of roughly the following
format:

```text
YYYY-MM-DD[Tt]HH:mm:ss[.nn][Zz][+-]HH:mm
```

And returns the time that was represented by the RFC 3339 string, with the UTC
offset applied to it.

**Inputs**:
- `rfc_datetime`: An RFC 3339 string to parse.
- `is_leap`: Optional output parameter specifying whether the moment was a leap
  second.

**Returns**:
- `res`: The time, with UTC offset applied, that was parsed from the RFC 3339
  string.
- `consumed`: The number of bytes consumed by parsing the RFC 3339 string.

**Notes**:
- Only 4-digit years are accepted.
- Leap seconds are smeared into 23:59:59.
*/
@(require_results)
rfc3339_to_time_utc :: proc(rfc_datetime: string, is_leap: ^bool = nil) -> (res: Time, consumed: int) {
	offset: int

	res, offset, consumed = rfc3339_to_time_and_offset(rfc_datetime, is_leap)
	res._nsec += (i64(-offset) * i64(Minute))
	return res, consumed
}

/*
Parse an RFC 3339 string into a time and a UTC offset in minutes.

This procedure parses the specified RFC 3339 strings of roughly the following
format:

```text
YYYY-MM-DD[Tt]HH:mm:ss[.nn][Zz][+-]HH:mm
```

And returns the time, in UTC and a UTC offset, in minutes, that were represented
by the RFC 3339 string.

**Inputs**:
- `rfc_datetime`: The RFC 3339 string to be parsed.
- `is_leap`: Optional output parameter specifying whether the moment was a
  leap second.

**Returns**:
- `res`: The time, in UTC, that was parsed from the RFC 3339 string.
- `utc_offset`: The UTC offset, in minutes, that was parsed from the RFC 3339
  string.
- `consumed`: The number of bytes consumed by parsing the string.

**Notes**:
- Only 4-digit years are accepted.
- Leap seconds are smeared into 23:59:59.
*/
@(require_results)
rfc3339_to_time_and_offset :: proc(rfc_datetime: string, is_leap: ^bool = nil) -> (res: Time, utc_offset: int, consumed: int) {
	moment, offset, leap_second, count := rfc3339_to_components(rfc_datetime)
	if count == 0 {
		return
	}

	if is_leap != nil {
		is_leap^ = leap_second
	}

	if _res, ok := datetime_to_time(moment.year, moment.month, moment.day, moment.hour, moment.minute, moment.second, moment.nano); !ok {
		return {}, 0, 0
	} else {
		return _res, offset, count
	}
}

/*
Parse an RFC 3339 string into a datetime and a UTC offset in minutes.

This procedure parses the specified RFC 3339 strings of roughly the following
format:

```text
YYYY-MM-DD[Tt]HH:mm:ss[.nn][Zz][+-]HH:mm
```

And returns the datetime, in UTC and the UTC offset, in minutes, that were
represented by the RFC 3339 string.

**Inputs**:
- `rfc_datetime`: The RFC 3339 string to parse.

**Returns**:
- `res`: The datetime, in UTC, that was parsed from the RFC 3339 string.
- `utc_offset`: The UTC offset, in minutes, that was parsed from the RFC 3339
  string.
- `is_leap`: Specifies whether the moment was a leap second.
- `consumed`: Number of bytes consumed by parsing the string.

Performs no validation on whether components are valid, e.g. it'll return hour = 25 if that's what it's given
*/
@(require_results)
rfc3339_to_components :: proc(rfc_datetime: string) -> (res: dt.DateTime, utc_offset: int, is_leap: bool, consumed: int) {
	moment, offset, count, leap_second, ok := _rfc3339_to_components(rfc_datetime)
	if !ok {
		return
	}
	return moment, offset, leap_second, count
}

// Parses an RFC 3339 string and returns datetime.DateTime.
// Performs no validation on whether components are valid, e.g. it'll return hour = 25 if that's what it's given
@(private)
_rfc3339_to_components :: proc(rfc_datetime: string) -> (res: dt.DateTime, utc_offset: int, consumed: int, is_leap: bool, ok: bool) {
	// A compliant date is at minimum 20 characters long, e.g. YYYY-MM-DDThh:mm:ssZ
	(len(rfc_datetime) >= 20) or_return

	// Scan and eat YYYY-MM-DD[Tt], then scan and eat HH:MM:SS, leave separator
	year   := scan_digits(rfc_datetime[0:], "-",   4) or_return
	month  := scan_digits(rfc_datetime[5:], "-",   2) or_return
	day    := scan_digits(rfc_datetime[8:], "Tt ", 2) or_return
	hour   := scan_digits(rfc_datetime[11:], ":",  2) or_return
	minute := scan_digits(rfc_datetime[14:], ":",  2) or_return
	second := scan_digits(rfc_datetime[17:], "",   2) or_return
	nanos  := 0
	count  := 19

	if rfc_datetime[count] == '.' {
		// Scan hundredths. The string must be at least 4 bytes long (.hhZ)
		(len(rfc_datetime[count:]) >= 4) or_return
		hundredths := scan_digits(rfc_datetime[count+1:], "", 2) or_return
		count += 3
		nanos = 10_000_000 * hundredths
	}

	// Leap second handling
	if minute == 59 && second == 60 {
		second = 59
		is_leap = true
	}

	err: dt.Error
	if res, err = dt.components_to_datetime(year, month, day, hour, minute, second, nanos); err != .None {
		return {}, 0, 0, false, false
	}

	// Scan UTC offset
	switch rfc_datetime[count] {
	case 'Z', 'z':
		utc_offset = 0
		count += 1
	case '+', '-':
		(len(rfc_datetime[count:]) >= 6) or_return
		offset_hour   := scan_digits(rfc_datetime[count+1:], ":", 2) or_return
		offset_minute := scan_digits(rfc_datetime[count+4:], "",  2) or_return

		utc_offset = 60 * offset_hour + offset_minute
		utc_offset *= -1 if rfc_datetime[count] == '-' else 1
		count += 6
	}
	return res, utc_offset, count, is_leap, true
}

@(private)
scan_digits :: proc(s: string, sep: string, count: int) -> (res: int, ok: bool) {
	needed := count + min(1, len(sep))
	(len(s) >= needed) or_return

	#no_bounds_check for i in 0..<count {
		if v := s[i]; v >= '0' && v <= '9' {
			res = res * 10 + int(v - '0')
		} else {
			return 0, false
		}
	}
	found_sep := len(sep) == 0
	#no_bounds_check for v in sep {
		found_sep |= rune(s[count]) == v
	}
	return res, found_sep
}

/*
Serialize the timestamp as a RFC 3339 string.

The boolean `ok` is false if the `time` is not a valid datetime, or if allocating the result string fails.

**Inputs**:
- `utc_offset`: offset in minutes wrt UTC (ie. the timezone)
- `include_nanos`: whether to include nanoseconds in the result.
*/
@(require_results)
time_to_rfc3339 :: proc(time: Time, utc_offset : int = 0, include_nanos := true, allocator := context.allocator) -> (res: string, ok: bool) {
	utc_offset := utc_offset

	// convert to datetime
	datetime := time_to_datetime(time) or_return

	if datetime.year < 0 || datetime.year >= 10_000 { return "", false }

	temp_string := [36]u8{}
	offset : uint = 0

	print_as_fixed_int :: proc(dst: []u8, offset: ^uint, width: i8, i: i64) {
		i := i
		width := width
		for digit_idx in 0..<width {
			last_digit := i % 10
			dst[offset^ + uint(width) - uint(digit_idx)-1] = '0' + u8(last_digit)
			i = i / 10
		}

		offset^ += uint(width)
	}

	print_as_fixed_int(temp_string[:], &offset, 4, datetime.year)
	temp_string[offset] = '-'
	offset += 1
	print_as_fixed_int(temp_string[:], &offset, 2, i64(datetime.month))
	temp_string[offset] = '-'
	offset += 1
	print_as_fixed_int(temp_string[:], &offset, 2, i64(datetime.day))
	temp_string[offset] = 'T'
	offset += 1
	print_as_fixed_int(temp_string[:], &offset, 2, i64(datetime.hour))
	temp_string[offset] = ':'
	offset += 1
	print_as_fixed_int(temp_string[:], &offset, 2, i64(datetime.minute))
	temp_string[offset] = ':'
	offset += 1
	print_as_fixed_int(temp_string[:], &offset, 2, i64(datetime.second))

	// turn 123_450_000 to 12345, 5
	strip_trailing_zeroes_nanos :: proc(n: i64) -> (res: i64, n_digits: i8) {
		res = n
		n_digits = 9
		for res % 10 == 0 {
			res = res / 10
			n_digits -= 1
		}
		return
	}

	// pre-epoch times: turn, say, -400ms to +600ms for display
	nanos := time._nsec % 1_000_000_000
	if nanos < 0 {
		nanos += 1_000_000_000
	}

	if nanos != 0 && include_nanos {
		temp_string[offset] = '.'
		offset += 1

		// remove trailing zeroes
		nanos_nonzero, n_digits := strip_trailing_zeroes_nanos(nanos)
		assert(nanos_nonzero != 0)

		// write digits, right-to-left
		for digit_idx : i8 = n_digits-1; digit_idx >= 0; digit_idx -= 1 {
			digit := u8(nanos_nonzero % 10)
			temp_string[offset + uint(digit_idx)] = '0' + u8(digit)
			nanos_nonzero /= 10
		}
		offset += uint(n_digits)
	}

	if utc_offset == 0 {
		temp_string[offset] = 'Z'
		offset += 1
	} else {
		temp_string[offset] = utc_offset > 0 ? '+' : '-'
		offset += 1
		utc_offset = abs(utc_offset)
		print_as_fixed_int(temp_string[:], &offset, 2, i64(utc_offset / 60))
		temp_string[offset] = ':'
		offset += 1
		print_as_fixed_int(temp_string[:], &offset, 2, i64(utc_offset % 60))
	}

	res_as_slice, res_alloc := make_slice([]u8, len=offset, allocator = allocator)
	if res_alloc != nil {
		return "", false
	}

	copy(res_as_slice, temp_string[:offset])

	return string(res_as_slice), true
}