oficina / sheets Public

Clone
git clone https://amadan.net/oficina/sheets

Plain git — no account needed to clone.

Download

Download this file

1package calc
2
3import (
4 "math"
5 "time"
6)
7
8// fn_date_parts.go: the date functions that take a value rather than the
9// clock.
10//
11// **This engine uses a PROLEPTIC 1900 system, and Excel does not.**
12// Excel's serial 60 is 1900-02-29 — a day that did not exist, kept for
13// compatibility with Lotus 1-2-3 — so every Excel serial from 60 onward
14// is one greater than a correct day count from 1900-01-01. This engine
15// counts real days from 1899-12-30 (fn_date.go's excelEpoch), which
16// makes it agree with Excel for every date from 1900-03-01 and disagree
17// below it: DATE(1900,1,1) is 2 here and 1 in Excel, and
18// DATE(1900,2,29) normalises to March 1 rather than existing.
19//
20// Named here rather than fixed, deliberately. The epoch is shipped and
21// NOW/TODAY already round-trip through it, XLSX import and export both
22// carry serials through the same constant, and changing it moves every
23// stored date in every sheet. That is a migration and a spec ruling,
24// not a patch to a new file. Recorded in the slice spec's deferred
25// table with a trigger: a sheet whose dates predate 1900-03-01.
26//
27// The branch review found this by noticing that every corpus case
28// round-trips a modern DATE through YEAR/MONTH/DAY, so the offset
29// cancels and the oracle stays green. That is true, and it is why the
30// divergence is written down here rather than discovered later.
31//
32// NOW and TODAY are in fn_date.go and are clockFuncs, because they need
33// the pass's pinned instant. These need nothing but their arguments, so
34// they are ordinary Funcs — and keeping the two kinds in separate files
35// is what stops somebody adding a sixth one here and reaching for
36// env.Now() out of habit, which is the bug
37// TestClockIsPinnedWithinAPass was written for.
38//
39// A date is a number. fn_date.go's excelEpoch comment states the rule
40// and it holds here unchanged: there is no date Value kind, only a
41// serial that a number format displays as a calendar date.
42
43func init() {
44 Register("YEAR", fnYear)
45 Register("MONTH", fnMonth)
46 Register("DAY", fnDay)
47 Register("DATE", fnDate)
48 Register("EOMONTH", fnEOMonth)
49}
50
51// serialTime is dateSerial's inverse.
52//
53// Truncated toward zero before conversion, so the time of day inside a
54// serial is discarded rather than rounded: 2026-09-03 23:59 must be
55// DAY 3, and a serial rounded to the nearest whole day would make it
56// the 4th. That is the one arithmetic mistake available here and it is
57// invisible for all but the last hour of a day, which is when nobody
58// is looking.
59func serialTime(serial float64) time.Time {
60 return excelEpoch.AddDate(0, 0, int(math.Trunc(serial)))
61}
62
63// datePart is the shared body of YEAR, MONTH and DAY.
64//
65// A negative serial is #NUM!, not a date before 1899: Excel has no
66// negative dates, and AddDate would cheerfully produce one that then
67// formats as something plausible.
68func datePart(args []Value, part func(time.Time) int) Value {
69 if len(args) != 1 {
70 return errValue(ErrValue)
71 }
72 if args[0].Kind == KindErr {
73 return args[0]
74 }
75 serial, ek := toNumber(args[0])
76 if ek != ErrNone {
77 return errValue(ek)
78 }
79 if serial < 0 {
80 return errValue(ErrNum)
81 }
82 return Value{Kind: KindNum, Num: float64(part(serialTime(serial)))}
83}
84
85// maxSheetYear is the last year Excel's date system represents.
86const maxSheetYear = 9999
87
88func fnYear(args []Value) Value {
89 return datePart(args, func(t time.Time) int { return t.Year() })
90}
91
92func fnMonth(args []Value) Value {
93 return datePart(args, func(t time.Time) int { return int(t.Month()) })
94}
95
96func fnDay(args []Value) Value {
97 return datePart(args, func(t time.Time) int { return t.Day() })
98}
99
100// fnDate implements DATE(year, month, day).
101//
102// Out-of-range months and days ROLL, and that is Excel's documented
103// behaviour rather than a shortcut: DATE(2026,13,1) is January 2027 and
104// DATE(2026,1,0) is the last day of December 2025. time.Date normalises
105// exactly this way, which is why the arguments are passed through
106// rather than validated — validating them would be a divergence dressed
107// as strictness, and `=DATE(YEAR(A1),MONTH(A1)+1,0)` is the idiom half
108// of all end-of-month formulas are written with.
109func fnDate(args []Value) Value {
110 if len(args) != 3 {
111 return errValue(ErrValue)
112 }
113 parts := make([]int, 3)
114 for i, a := range args {
115 if a.Kind == KindErr {
116 return a
117 }
118 n, ek := toNumber(a)
119 if ek != ErrNone {
120 return errValue(ek)
121 }
122 parts[i] = int(math.Trunc(n))
123 }
124 // Excel's two-digit year window: 0-1899 is read as an offset from
125 // 1900, so DATE(26,9,3) is 1926 and not the year 26. Written out
126 // rather than left to time.Date, which would happily build a
127 // first-century date that then formats as a plausible-looking
128 // serial nobody meant.
129 year := parts[0]
130 if year >= 0 && year < 1900 {
131 year += 1900
132 }
133 // Excel's own upper bound. time.Date will build the year 400000
134 // without complaint and dateSerial will return a number for it,
135 // which is a value no spreadsheet can hold and no export can carry.
136 if year > maxSheetYear {
137 return errValue(ErrNum)
138 }
139 t := time.Date(year, time.Month(parts[1]), parts[2], 0, 0, 0, 0, time.UTC)
140 if t.Before(excelEpoch) {
141 return errValue(ErrNum)
142 }
143 return Value{Kind: KindNum, Num: dateSerial(t)}
144}
145
146// fnEOMonth implements EOMONTH(start_date, months): the last day of the
147// month `months` away from start_date.
148//
149// Computed as day zero of the FOLLOWING month rather than by asking how
150// many days the target month has. Both are correct; this one has no
151// leap-year branch to get wrong, and time.Date's normalisation is
152// already the thing being relied on in fnDate above.
153func fnEOMonth(args []Value) Value {
154 if len(args) != 2 {
155 return errValue(ErrValue)
156 }
157 for _, a := range args {
158 if a.Kind == KindErr {
159 return a
160 }
161 }
162 start, ek := toNumber(args[0])
163 if ek != ErrNone {
164 return errValue(ek)
165 }
166 months, ek := toNumber(args[1])
167 if ek != ErrNone {
168 return errValue(ek)
169 }
170 if start < 0 {
171 return errValue(ErrNum)
172 }
173 t := serialTime(start)
174 end := time.Date(t.Year(), t.Month()+time.Month(int(math.Trunc(months)))+1, 0,
175 0, 0, 0, 0, time.UTC)
176 if end.Before(excelEpoch) {
177 return errValue(ErrNum)
178 }
179 return Value{Kind: KindNum, Num: dateSerial(end)}
180}
181