aboutsummaryrefslogtreecommitdiff
path: root/core/time/iso8601.odin
blob: f00107226a1570ab80c7b7931833a8c27f7a5538 (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
package time
// Parsing ISO 8601 date/time strings into time.Time.

import dt "core:time/datetime"

/*
Parse an ISO 8601 string into a time with UTC offset applied to it.

This procedure parses an ISO 8601 string of roughly the following format:

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

And returns time, in UTC represented by that string. In case the timezone offset
is specified in the string, that timezone is applied to time.

**Inputs**:
- `iso_datetime`: The string to be parsed.
- `is_leap`: Optional output parameter, specifying if the moment was a leap second.

**Returns**:
- `res`: The time represented by `iso_datetime`, with UTC offset applied.
- `consumed`: Number of bytes consumed by parsing the string.

**Notes**:
- Only 4-digit years are accepted.
- Leap seconds are smeared into 23:59:59.
*/
iso8601_to_time_utc :: proc(iso_datetime: string, is_leap: ^bool = nil) -> (res: Time, consumed: int) {
	offset: int
	res, offset, consumed = iso8601_to_time_and_offset(iso_datetime, is_leap)
	res._nsec += (i64(-offset) * i64(Minute))
	return res, consumed
}

/*
Parse an ISO 8601 string into a time and a UTC offset in minutes.

This procedure parses an ISO 8601 string of roughly the following format:

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

And returns time, in UTC represented by that string, and the UTC offset, in
minutes.

**Inputs**:
- `iso_datetime`: The string to be parsed.
- `is_leap`: Optional output parameter, specifying if the moment was a leap second.

**Returns**:
- `res`: The time in UTC.
- `utc_offset`: The UTC offset of the time, in minutes.
- `consumed`: Number of bytes consumed by parsing the string.

**Notes**:
- Only 4-digit years are accepted.
- Leap seconds are smeared into 23:59:59.
*/
iso8601_to_time_and_offset :: proc(iso_datetime: string, is_leap: ^bool = nil) -> (res: Time, utc_offset: int, consumed: int) {
	moment, offset, leap_second, count := iso8601_to_components(iso_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 ISO 8601 string into a datetime and a UTC offset in minutes.

This procedure parses an ISO 8601 string of roughly the following format:

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

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

**Inputs**:
- `iso_datetime`: The string to be parsed

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

**Notes**:
- This procedure performs no validation on whether components are valid,
  e.g. it'll return hour = 25 if that's what it's given in the specified
  string.
*/
iso8601_to_components :: proc(iso_datetime: string) -> (res: dt.DateTime, utc_offset: int, is_leap: bool, consumed: int) {
	moment, offset, count, leap_second, ok := _iso8601_to_components(iso_datetime)
	if !ok {
		return
	}
	return moment, offset, leap_second, count
}

// Parses an ISO 8601 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)
_iso8601_to_components :: proc(iso_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(iso_datetime) >= 20) or_return

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

	// Scan fractional seconds
	if iso_datetime[count] == '.' {
		count += 1 // consume '.'
		multiplier := 100_000_000
		for digit in iso_datetime[count:] {
			if multiplier >= 1 && int(digit) >= '0' && int(digit) <= '9' {
				nanos += int(digit - '0') * multiplier
				multiplier /= 10
				count += 1
			} else {
				break
			}
		}
	}

	// 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
	}

	if len(iso_datetime[count:]) == 0 {
		return res, utc_offset, count, is_leap, true
	}

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

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