Skip to content

Commit ffa53a1

Browse files
authored
Implement OffsetTime and OffsetDateTime (#65)
* Implement Offset. Signed-off-by: joe-mann <[email protected]> * Rename test. Signed-off-by: joe-mann <[email protected]> * Rename test. Signed-off-by: joe-mann <[email protected]> * Add test for LocalTime.String. Signed-off-by: joe-mann <[email protected]> * Implement OffsetTime. Signed-off-by: joe-mann <[email protected]> * Initial OffsetDateTime implementation. Signed-off-by: joe-mann <[email protected]> * Fix ISO8601TimeOffsetExtended. Signed-off-by: joe-mann <[email protected]> * Tidy up and add tests. Signed-off-by: joe-mann <[email protected]> * Fix example name. Signed-off-by: joe-mann <[email protected]> * Please linter. Signed-off-by: joe-mann <[email protected]> * Add missing error check. Signed-off-by: joe-mann <[email protected]> --------- Signed-off-by: joe-mann <[email protected]>
1 parent a79c6fe commit ffa53a1

26 files changed

+2047
-496
lines changed

.github/workflows/push.yaml

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ jobs:
66
steps:
77
- uses: actions/setup-go@v3
88
with:
9-
go-version: 1.16.x
9+
go-version: 1.21.x
1010
- uses: actions/checkout@v3
1111
- uses: golangci/golangci-lint-action@v3
1212
test:
@@ -15,6 +15,6 @@ jobs:
1515
steps:
1616
- uses: actions/setup-go@v3
1717
with:
18-
go-version: 1.16.x
18+
go-version: 1.21.x
1919
- uses: actions/checkout@v2
2020
- run: go test -v -count 1 ./...

date.go

+290
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
package chrono
2+
3+
import (
4+
"fmt"
5+
"math"
6+
"math/big"
7+
)
8+
9+
func getISOWeek(v int64) (isoYear, isoWeek int, err error) {
10+
year, month, day, err := fromDate(v)
11+
if err != nil {
12+
return 0, 0, err
13+
}
14+
15+
isoYear = year
16+
isoWeek = int((10 + getOrdinalDate(isoYear, int(month), day) - getWeekday(int32(v))) / 7)
17+
if isoWeek == 0 {
18+
if isLeapYear(isoYear - 1) {
19+
return isoYear - 1, 53, nil
20+
}
21+
return isoYear - 1, 52, nil
22+
}
23+
24+
if isoWeek == 53 && !isLeapYear(year) {
25+
return isoYear + 1, 1, nil
26+
}
27+
28+
return isoYear, isoWeek, nil
29+
}
30+
31+
var daysInMonths = [12]int{
32+
January - 1: 31,
33+
February - 1: 28,
34+
March - 1: 31,
35+
April - 1: 30,
36+
May - 1: 31,
37+
June - 1: 30,
38+
July - 1: 31,
39+
August - 1: 31,
40+
September - 1: 30,
41+
October - 1: 31,
42+
November - 1: 30,
43+
December - 1: 31,
44+
}
45+
46+
const (
47+
// unixEpochJDN is the JDN that corresponds to 1st January 1970 (Gregorian).
48+
unixEpochJDN = 2440588
49+
50+
// The minimum representable date is JDN 0.
51+
minYear = -4713
52+
minMonth = int(November)
53+
minDay = 24
54+
minJDN = -unixEpochJDN
55+
56+
// The maximum representable date must fit into an int32.
57+
maxYear = 5874898
58+
maxMonth = int(June)
59+
maxDay = 3
60+
maxJDN = math.MaxInt32 - unixEpochJDN
61+
)
62+
63+
func getWeekday(ordinal int32) int {
64+
return int((ordinal+int32(unixEpochJDN))%7) + 1
65+
}
66+
67+
func isLeapYear(year int) bool {
68+
return (year%4 == 0 && year%100 != 0) || year%400 == 0
69+
}
70+
71+
func getOrdinalDate(year, month, day int) int {
72+
var out int
73+
for i := int(January); i <= month; i++ {
74+
if i == month {
75+
out += int(day)
76+
} else {
77+
out += int(daysInMonths[i-1])
78+
}
79+
}
80+
81+
if isLeapYear(year) && month > int(February) {
82+
out++
83+
}
84+
return out
85+
}
86+
87+
func isDateInBounds(year, month, day int) bool {
88+
if year < minYear {
89+
return false
90+
} else if year == minYear {
91+
if month < minMonth {
92+
return false
93+
} else if month == minMonth && day < minDay {
94+
return false
95+
}
96+
}
97+
98+
if year > maxYear {
99+
return false
100+
} else if year == maxYear {
101+
if month > maxMonth {
102+
return false
103+
} else if month == maxMonth && day > maxDay {
104+
return false
105+
}
106+
}
107+
108+
return true
109+
}
110+
111+
func isDateValid(year, month, date int) bool {
112+
if month < int(January) || month > int(December) {
113+
return false
114+
}
115+
116+
if isLeapYear(year) && month == int(February) {
117+
return date > 0 && date <= 29
118+
}
119+
return date > 0 && date <= daysInMonths[month-1]
120+
}
121+
122+
func fromDate(v int64) (year, month, day int, err error) {
123+
if v < minJDN || v > maxJDN {
124+
return 0, 0, 0, fmt.Errorf("invalid date")
125+
}
126+
127+
dd := int64(v + unixEpochJDN)
128+
129+
f := dd + 1401 + ((((4*dd + 274277) / 146097) * 3) / 4) - 38
130+
e := 4*f + 3
131+
g := (e % 1461) / 4
132+
h := 5*g + 2
133+
134+
day = int((h%153)/5) + 1
135+
month = int((h/153+2)%12) + 1
136+
year = int(e/1461 - 4716 + (14-int64(month))/12)
137+
return
138+
}
139+
140+
func getYearDay(v int64) (int, error) {
141+
year, month, day, err := fromDate(v)
142+
if err != nil {
143+
return 0, err
144+
}
145+
return getOrdinalDate(year, int(month), day), nil
146+
}
147+
148+
func getDaysInYear(year int) int {
149+
if isLeapYear(year) {
150+
return 366
151+
}
152+
return 365
153+
}
154+
155+
func makeDate(year, month, day int) (int64, error) {
156+
if !isDateInBounds(year, month, day) {
157+
return 0, fmt.Errorf("date out of bounds")
158+
}
159+
return makeJDN(int64(year), int64(month), int64(day)), nil
160+
}
161+
162+
func makeJDN(y, m, d int64) int64 {
163+
return (1461*(y+4800+(m-14)/12))/4 + (367*(m-2-12*((m-14)/12)))/12 - (3*((y+4900+(m-14)/12)/100))/4 + d - 32075 - unixEpochJDN
164+
}
165+
166+
func ofDayOfYear(year, day int) (int64, error) {
167+
isLeap := isLeapYear(year)
168+
if (!isLeap && day > 365) || day > 366 {
169+
return 0, fmt.Errorf("invalid date")
170+
}
171+
172+
var month Month
173+
174+
var total int
175+
for m, n := range daysInMonths {
176+
if isLeap && m == 1 {
177+
n = 29
178+
}
179+
180+
if total+n >= day {
181+
day = day - total
182+
month = Month(m + 1)
183+
break
184+
}
185+
total += n
186+
}
187+
188+
return makeDate(year, int(month), day)
189+
}
190+
191+
func ofISOWeek(year, week, day int) (int64, error) {
192+
if week < 1 || week > 53 {
193+
return 0, fmt.Errorf("invalid week number")
194+
}
195+
196+
jan4th, err := makeDate(year, int(January), 4)
197+
if err != nil {
198+
return 0, err
199+
}
200+
201+
v := week*7 + int(day) - int(getWeekday(int32(jan4th))+3)
202+
203+
daysThisYear := getDaysInYear(year)
204+
switch {
205+
case v <= 0: // Date is in previous year.
206+
return ofDayOfYear(year-1, v+getDaysInYear(year-1))
207+
case v > daysThisYear: // Date is in next year.
208+
return ofDayOfYear(year+1, v-daysThisYear)
209+
default: // Date is in this year.
210+
return ofDayOfYear(year, v)
211+
}
212+
}
213+
214+
func addDateToDate(d int64, years, months, days int) (int64, error) {
215+
year, month, day, err := fromDate(d)
216+
if err != nil {
217+
return 0, err
218+
}
219+
220+
out, err := makeDate(year+years, int(month)+months, day+days)
221+
return out, err
222+
}
223+
224+
func simpleDateStr(year, month, day int) string {
225+
return fmt.Sprintf("%04d-%02d-%02d", year, month, day)
226+
}
227+
228+
func getISODateSimpleStr(year, week, day int) string {
229+
return fmt.Sprintf("%04d-W%02d-%d", year, week, day)
230+
}
231+
232+
func makeDateTime(date, time int64) big.Int {
233+
out := big.NewInt(date)
234+
out.Mul(out, bigIntDayExtent)
235+
out.Add(out, big.NewInt(time))
236+
return *out
237+
}
238+
239+
func addDurationToBigDate(d big.Int, v Duration) (big.Int, error) {
240+
out := new(big.Int).Set(&d)
241+
out.Add(out, &v.v)
242+
243+
if out.Cmp(&minLocalDateTime.v) == -1 || out.Cmp(&maxLocalDateTime.v) == 1 {
244+
return big.Int{}, fmt.Errorf("datetime out of range")
245+
}
246+
return *out, nil
247+
}
248+
249+
func bigDateToOffset(d big.Int, o1, o2 int64) big.Int {
250+
out := new(big.Int).Set(&d)
251+
out.Sub(out, big.NewInt(o1))
252+
out.Add(out, big.NewInt(o2))
253+
return *out
254+
}
255+
256+
func addDateToBigDate(d big.Int, years, months, days int) (big.Int, error) {
257+
date, _ := splitDateAndTime(d)
258+
259+
added, err := addDateToDate(date, years, months, days)
260+
if err != nil {
261+
return big.Int{}, err
262+
}
263+
264+
if added < minJDN || added > maxJDN {
265+
return big.Int{}, fmt.Errorf("date out of bounds")
266+
}
267+
268+
diff := big.NewInt(int64(added - date))
269+
diff.Mul(diff, bigIntDayExtent)
270+
271+
out := new(big.Int).Set(&d)
272+
out.Add(out, diff)
273+
274+
return *out, nil
275+
}
276+
277+
func splitDateAndTime(v big.Int) (date, time int64) {
278+
vv := new(big.Int).Set(&v)
279+
280+
var _time big.Int
281+
_date, _ := vv.DivMod(vv, bigIntDayExtent, &_time)
282+
return _date.Int64(), _time.Int64()
283+
}
284+
285+
var (
286+
bigIntDayExtent = big.NewInt(24 * int64(Hour))
287+
288+
minLocalDateTime = OfLocalDateTime(MinLocalDate(), LocalTimeOf(0, 0, 0, 0))
289+
maxLocalDateTime = OfLocalDateTime(MaxLocalDate(), LocalTimeOf(99, 59, 59, 999999999))
290+
)

docs/Functions.md

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Functions
2+
3+
The table below shows the functions attached to the primary date and time types of `chrono`. Where a function returns a `chrono` type, that type is specified.
4+
5+
| | `LocalDate` | `LocalTime` | `LocalDateTime` | `OffsetTime` | `OffsetDateTime` |
6+
| ---------------------------------------------- | :---------: | :---------: | :----------------------: | :----------: | :-----------------------: |
7+
| **`Date() (year int, month Month, day int)`** | 🗸 | | | | |
8+
| **`IsLeapYear() bool`** | 🗸 | | | | |
9+
| **`Weekday() Weekday`** | 🗸 | | | | |
10+
| **`YearDay() int`** | 🗸 | | | | |
11+
| **`ISOWeek() (isoYear, isoWeek int)`** | 🗸 | | | | |
12+
| **`Clock() (hour, min, sec int)`** | | 🗸 | | 🗸 | |
13+
| **`Nanosecond() int`** | | 🗸 | | 🗸 | |
14+
| **`BusinessHour() int`** | | 🗸 | | 🗸 | |
15+
| **`Offset() Offset`** | | | | 🗸 | 🗸 |
16+
| **`Split() ...`** | | | `LocalDate`, `LocalTime` | | `LocalDate`, `OffsetTime` |
17+
| **`Local() ...`** | | | | `LocalTime` | `LocalDateTime` |
18+
| **`In() ...`** | | | | `LocalTime` | `OffsetDateTime` |
19+
| **`UTC() ...`** | | | | `LocalTime` | `OffsetDateTime` |
20+
| **`Sub() ...`** | | 🗸 | 🗸 | 🗸 | 🗸 |
21+
| **`Add(...) ...`** | | `LocalTime` | `LocalDateTime` | `LocalTime` | `OffsetDateTime` |
22+
| **`CanAdd(...) bool`** | | 🗸 | 🗸 | 🗸 | 🗸 |
23+
| **`AddDate(years, months, days int) ...`** | `LocalDate` | | `LocalDateTime` | | `OffsetDateTime` |
24+
| **`CanAddDate(years, months, days int) bool`** | 🗸 | | 🗸 | | 🗸 |
25+
| **`Format(layout string) string`** | 🗸 | 🗸 | 🗸 | 🗸 | 🗸 |
26+
| **`Parse(layout, value string) error`** | 🗸 | 🗸 | 🗸 | 🗸 | 🗸 |

duration.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@ type Duration struct {
1616
// Durations and extents are semantically equivalent, except that durations exceed,
1717
// and can therefore not be converted to, Go's basic types. Extents are represented as a single integer.
1818
func DurationOf(v Extent) Duration {
19-
return Duration{v: *big.NewInt(int64(v))}
19+
return durationOf(int64(v))
20+
}
21+
22+
func durationOf(v int64) Duration {
23+
return Duration{v: *big.NewInt(v)}
2024
}
2125

2226
// Compare compares d with d2. If d is less than d2, it returns -1;

example_local_date_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,9 @@ func ExampleLocalDate_add_subtract() {
6262
// Output: 2007-05-25
6363
}
6464

65-
func ExampleLocalDate_Add() {
65+
func ExampleLocalDate_AddDate() {
6666
d := chrono.LocalDateOf(2007, chrono.May, 20)
67-
d = d.Add(0, 1, 1)
67+
d = d.AddDate(0, 1, 1)
6868

6969
fmt.Println(d)
7070
// Output: 2007-06-21

example_local_date_time_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ func ExampleLocalDateTimeOf() {
1313
// Output: 2007-05-20 12:30:15
1414
}
1515

16-
func ExampleOfLocalDateAndTime() {
16+
func ExampleOfLocalDateTime() {
1717
d := chrono.LocalDateOf(2007, chrono.May, 20)
1818
t := chrono.LocalTimeOf(12, 30, 15, 0)
1919

20-
dt := chrono.OfLocalDateAndTime(d, t)
20+
dt := chrono.OfLocalDateTime(d, t)
2121

2222
fmt.Println(dt)
2323
// Output: 2007-05-20 12:30:15

0 commit comments

Comments
 (0)