1package headless23import (4 "math"5 "regexp"6 "strconv"7 "time"89 "src.rest/cat/012/internal/fileio"10 "src.rest/cat/012/internal/nuon"11 "src.rest/cat/012/internal/sheet"12)1314// Reading a cell as a Value: its type from its format, so what reads15// it back writes the same value and format. A number in a format its16// type doesn't show by itself carries decimals or the format's code.1718// CellValue is the value of the cell at a, typed by the format it's19// shown in.20func CellValue(s *sheet.Sheet, a sheet.Addr) Value {21 return valueOf(s.Value(a), s.DisplayFormat(a))22}2324// valueOf is v, shown in f, as a Value.25func valueOf(v sheet.Value, f sheet.Format) Value {26 switch v.Kind {27 case sheet.Empty:28 return Value("null")29 case sheet.Bool:30 return Value(strconv.FormatBool(v.Num != 0))31 case sheet.Text:32 return Value(nuon.AppendJSON(nil, nuon.StringValue(v.Str)))33 case sheet.Number:34 return numberOf(v.Num, f)35 }36 return Value(nuon.AppendJSON(nil, nuon.StringValue(v.String()))) // an error, as it shows37}3839// plainJSON is n as a JSON number: an int when whole.40func plainJSON(n float64) nuon.Value {41 if n == math.Trunc(n) && math.Abs(n) < 1<<53 {42 return nuon.IntValue(int64(n))43 }44 return nuon.FloatValue(n)45}4647// numberOf is n shown in f: a plain number in the Automatic format, an48// object naming its type otherwise.49func numberOf(n float64, f sheet.Format) Value {50 kind, val := typeOf(n, f)51 if kind == "" {52 return Value(nuon.AppendJSON(nil, plainJSON(n)))53 }54 fs := []nuon.Field{{Key: kind, Value: val}}55 if kind == "currency" {56 if sym := currencySymbol(f); sym != "$" {57 fs = append(fs, nuon.Field{Key: "symbol", Value: nuon.StringValue(sym)})58 }59 }60 fs = append(fs, formatFields(fs, f)...)61 return Value(nuon.AppendJSON(nil, nuon.RecordValue(fs...)))62}6364// formatFields are the fields a Value of fields needs to be shown in f:65// none when its type shows it so, decimals when they're all it takes,66// and the format's code otherwise.67func formatFields(fs []nuon.Field, f sheet.Format) []nuon.Field {68 tries := [][]nuon.Field{nil}69 if f.Kind.HasDecimals() || currencySymbol(f) != "" {70 tries = append(tries, []nuon.Field{{Key: "decimals", Value: nuon.IntValue(int64(decimalsOf(f)))}})71 }72 for _, extra := range tries {73 if t, err := typedRecord(append(fs[:len(fs):len(fs)], extra...)); err == nil && t.format == f {74 return extra75 }76 }77 return []nuon.Field{{Key: "format", Value: nuon.StringValue(f.Code())}}78}7980// decimalsOf are the decimals f shows: its own, or a currency81// pattern's.82func decimalsOf(f sheet.Format) int {83 if m := currencyPatternRE.FindStringSubmatch(f.Pattern); f.Kind == sheet.FmtCustom && m != nil {84 return max(len(m[3])-1, 0)85 }86 return f.Decimals87}8889// typeOf is the type a number shown in f is written as, and its value:90// "" for a plain number.91func typeOf(n float64, f sheet.Format) (string, nuon.Value) {92 kind := f.Kind93 if kind == sheet.FmtCustom {94 if currencySymbol(f) != "" {95 return "currency", plainJSON(n)96 }97 kind = fileio.CodeKind(f.Pattern)98 }99 switch kind {100 case sheet.FmtAuto, sheet.FmtText:101 return "", nuon.Value{}102 case sheet.FmtCurrency, sheet.FmtAccounting:103 return "currency", plainJSON(n)104 case sheet.FmtPercent:105 return "percent", plainJSON(n)106 case sheet.FmtDate, sheet.FmtDateTime:107 return "date", nuon.StringValue(isoDate(n))108 case sheet.FmtTime:109 if n >= 0 && n < 1 {110 return "time", nuon.StringValue(timeOfDay(n))111 }112 return "date", nuon.StringValue(isoDate(n))113 case sheet.FmtDuration:114 return "duration", nuon.StringValue(durationLiteral(time.Duration(math.Round(n * 86400e9))))115 case sheet.FmtSize:116 return "size", plainJSON(n)117 }118 return "number", plainJSON(n)119}120121// currencyPatternRE is a custom format showing a currency: a symbol,122// quoted as currencyPattern writes it or one of the common ones bare,123// before a grouped amount.124var currencyPatternRE = regexp.MustCompile(`^(?:"([^"]+)"|([€£¥₹]))#,##0(\.0+)?$`)125126// currencySymbol is the symbol a currency format shows: $ for 012's127// Currency and Accounting, the quoted symbol of a pattern such as128// "€"#,##0.00, "" for other formats.129func currencySymbol(f sheet.Format) string {130 switch f.Kind {131 case sheet.FmtCurrency, sheet.FmtAccounting:132 return "$"133 case sheet.FmtCustom:134 if m := currencyPatternRE.FindStringSubmatch(f.Pattern); m != nil {135 return m[1] + m[2]136 }137 }138 return ""139}140141// isoDate is a date serial in ISO 8601, on the sheet's clock: the day142// alone when it's whole, else with the time, to the microsecond.143func isoDate(n float64) string {144 days := math.Floor(n)145 us := math.Round((n - days) * 86400e6)146 if math.Abs(days) > 1e7 {147 days = math.Copysign(1e7, days)148 }149 t := time.Date(1899, 12, 30+int(days), 0, 0, 0, int(us)*1e3, time.UTC)150 if us == 0 && n == days {151 return t.Format("2006-01-02")152 }153 return t.Format("2006-01-02T15:04:05.999999")154}155156// timeOfDay is a fraction of a day as a time on the clock: 14:30:00.157func timeOfDay(n float64) string {158 us := math.Round(n * 86400e6)159 return time.Date(2000, 1, 1, 0, 0, 0, int(us)*1e3, time.UTC).Format("15:04:05.999999")160}161162// durationUnits are nushell's units of time, largest first.163var durationUnits = []struct {164 name string165 d time.Duration166}{{"day", 24 * time.Hour}, {"hr", time.Hour}, {"min", time.Minute}, {"sec", time.Second}, {"ms", time.Millisecond}, {"us", time.Microsecond}}167168// durationLiteral is d as nushell writes a duration literal, in the169// largest unit that holds it whole: 90min, 1day, 1500ms.170func durationLiteral(d time.Duration) string {171 for _, u := range durationUnits {172 if d%u.d == 0 {173 return strconv.FormatInt(int64(d/u.d), 10) + u.name174 }175 }176 return strconv.FormatInt(int64(d), 10) + "ns"177}