cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

cat/module-path
012 / internal / fileio / sourcecell.go
sourcecell.go 7.2K
1package fileio23import (4 "encoding/binary"5 "fmt"6 "math"7 "strings"8 "time"9 "unicode/utf8"1011 "github.com/parquet-go/parquet-go"12 "github.com/parquet-go/parquet-go/format"1314 "src.rest/cat/012/internal/sheet"15)1617// How Parquet and SQLite values become cells, shared by imports (which18// store them through the builder) and sources (which hand them out as19// they are). Text a file holds as a value comes out raw: an import20// stores it as typed in its locale (textOrDate) and a source reads the21// dates in it (typed), so both type a value the same way. Text 01222// makes (joined lists, a blob's size) stays text.2324// fileCell is a value from a file, raw when it is text to be read as a25// date where it is one.26type fileCell struct {27 sheet.LiveCell28 raw bool29}3031// liveNum is a number in format f; NaN and infinities, which no cell32// holds, are #NUM!.33func liveNum(v float64, f sheet.Format) sheet.LiveCell {34 if math.IsNaN(v) || math.IsInf(v, 0) {35 return sheet.LiveCell{V: sheet.ErrNum}36 }37 return sheet.LiveCell{V: sheet.Value{Kind: sheet.Number, Num: v}, F: f}38}3940func strCell(s string) sheet.LiveCell {41 return sheet.LiveCell{V: sheet.Value{Kind: sheet.Text, Str: s}}42}4344func liveBool(b bool) sheet.LiveCell {45 v := sheet.Value{Kind: sheet.Bool}46 if b {47 v.Num = 148 }49 return sheet.LiveCell{V: v}50}5152// put stores a cell as an import does: raw text through textOrDate,53// which reads dates in the import's locale.54func (c fileCell) put(b *builder, a sheet.Addr) {55 switch c.V.Kind {56 case sheet.Empty:57 case sheet.Number:58 b.number(a, c.V.Num, c.F, sheet.Style{})59 case sheet.Bool:60 b.boolean(a, c.V.Num != 0, sheet.Style{})61 case sheet.Text:62 if c.raw {63 textOrDate(b, a, c.V.Str)64 } else {65 b.text(a, c.V.Str, c.F, sheet.Style{})66 }67 default:68 b.number(a, math.NaN(), c.F, sheet.Style{})69 }70}7172// typed is the cell as a source hands it out: raw text that is a date73// or time (2026-09-26) as that date, as an import would store it, and74// other text, numbers stored as text included, as text.75func (c fileCell) typed() sheet.LiveCell {76 if !c.raw {77 return c.LiveCell78 }79 s := flatten(c.V.Str)80 if s == "" {81 return sheet.LiveCell{} // blank, as an import leaves it82 }83 if n, f, ok := sheet.ParseValue(s); ok {84 switch f.Kind {85 case sheet.FmtDate, sheet.FmtTime, sheet.FmtDateTime:86 return sheet.LiveCell{V: sheet.Value{Kind: sheet.Number, Num: n}, F: f}87 }88 }89 return strCell(s)90}9192// parquetCells is a row's values of one column as a cell: empty for93// null, several joined with commas (reporting that they were).94func parquetCells(vals []parquet.Value, c parquetColumn) (cell fileCell, repeated bool) {95 if len(vals) == 1 {96 if vals[0].IsNull() {97 return fileCell{}, false98 }99 return parquetCell(vals[0], c), false100 }101 parts := make([]string, 0, len(vals))102 for _, v := range vals {103 if !v.IsNull() {104 parts = append(parts, parquetText(v, c))105 }106 }107 if len(parts) == 0 {108 return fileCell{}, false109 }110 return fileCell{LiveCell: strCell(strings.Join(parts, ", "))}, true111}112113// parquetCell is one value that isn't null.114func parquetCell(v parquet.Value, c parquetColumn) fileCell {115 lc, ok := parquetValue(v, c)116 if !ok {117 return fileCell{LiveCell: strCell(parquetText(v, c)), raw: true}118 }119 return fileCell{LiveCell: lc}120}121122// parquetValue is a value that isn't text, and false for text.123func parquetValue(v parquet.Value, c parquetColumn) (sheet.LiveCell, bool) {124 switch lt := c.logical.(type) {125 case *format.DateType:126 return liveNum(float64(v.Int32())+serialOf(time.Unix(0, 0).UTC()), sheet.Format{Kind: sheet.FmtDate}), true127 case *format.TimestampType:128 return liveNum(serialOf(timestamp(v.Int64(), lt.Unit)), sheet.Format{Kind: sheet.FmtDateTime}), true129 case *format.TimeType:130 d := timeUnit(lt.Unit) * time.Duration(timeInt(v))131 return liveNum(d.Hours()/24, sheet.Format{Kind: sheet.FmtTime}), true132 case *format.DecimalType:133 if x, ok := decimal(v, lt.Scale); ok {134 return liveNum(x, decimalFormat(lt)), true135 }136 }137 if c.interval {138 if d, ok := interval(v); ok {139 return liveNum(d, sheet.Format{Kind: sheet.FmtDuration}), true140 }141 }142 switch c.kind {143 case parquet.Boolean:144 return liveBool(v.Boolean()), true145 case parquet.Int32:146 return liveNum(float64(v.Int32()), sheet.Format{}), true147 case parquet.Int64:148 return liveNum(float64(v.Int64()), sheet.Format{}), true149 case parquet.Float:150 return liveNum(float64(v.Float()), sheet.Format{}), true151 case parquet.Double:152 return liveNum(v.Double(), sheet.Format{}), true153 case parquet.Int96:154 // Legacy timestamps: nanoseconds of the day, then the Julian day.155 i := v.Int96()156 nanos := int64(i[1])<<32 | int64(i[0])157 days := int64(i[2]) - 2440588 // the Julian day of 1970-01-01158 return liveNum(serialOf(time.Unix(days*86400, nanos).UTC()), sheet.Format{Kind: sheet.FmtDateTime}), true159 }160 return sheet.LiveCell{}, false161}162163// parquetFormat is the format a column's type shows its values in, and164// whether it holds numbers.165func parquetFormat(c parquetColumn) (sheet.Format, bool) {166 switch c.logical.(type) {167 case *format.DateType:168 return sheet.Format{Kind: sheet.FmtDate}, true169 case *format.TimestampType:170 return sheet.Format{Kind: sheet.FmtDateTime}, true171 case *format.TimeType:172 return sheet.Format{Kind: sheet.FmtTime}, true173 case *format.DecimalType:174 return decimalFormat(c.logical.(*format.DecimalType)), true175 }176 if c.interval {177 return sheet.Format{Kind: sheet.FmtDuration}, true178 }179 switch c.kind {180 case parquet.Int32, parquet.Int64, parquet.Float, parquet.Double:181 return sheet.Format{}, true182 case parquet.Int96:183 return sheet.Format{Kind: sheet.FmtDateTime}, true184 }185 return sheet.Format{}, false186}187188// decimalFormat is a DECIMAL's: a number with its scale's decimals.189func decimalFormat(t *format.DecimalType) sheet.Format {190 return sheet.Format{Kind: sheet.FmtNumber, Decimals: min(max(int(t.Scale), 0), sheet.MaxDecimals)}191}192193// interval is an INTERVAL's length in days: its months as 30 days each,194// its days, and its milliseconds.195func interval(v parquet.Value) (float64, bool) {196 b := v.ByteArray()197 if len(b) != 12 {198 return 0, false199 }200 months, days, ms := binary.LittleEndian.Uint32(b), binary.LittleEndian.Uint32(b[4:]), binary.LittleEndian.Uint32(b[8:])201 return 30*float64(months) + float64(days) + float64(ms)/86_400_000, true202}203204// sqliteCell is a database value as a cell, reporting whether it was205// binary data that could only be described.206func sqliteCell(v any) (cell fileCell, blob bool) {207 switch v := v.(type) {208 case nil:209 return fileCell{}, false210 case int64:211 return fileCell{LiveCell: liveNum(float64(v), sheet.Format{})}, false212 case float64:213 return fileCell{LiveCell: liveNum(v, sheet.Format{})}, false214 case bool:215 return fileCell{LiveCell: liveBool(v)}, false216 case time.Time:217 f := sheet.Format{Kind: sheet.FmtDateTime}218 if h, m, s := v.Clock(); h == 0 && m == 0 && s == 0 && v.Nanosecond() == 0 {219 f = sheet.Format{Kind: sheet.FmtDate}220 }221 return fileCell{LiveCell: liveNum(serialOf(v), f)}, false222 case string:223 return fileCell{LiveCell: strCell(v), raw: true}, false224 case []byte:225 if utf8.Valid(v) {226 return fileCell{LiveCell: strCell(string(v)), raw: true}, false227 }228 return fileCell{LiveCell: strCell(fmt.Sprintf("(%s)", count(len(v), "byte", "bytes")))}, true229 }230 return fileCell{LiveCell: strCell(fmt.Sprint(v))}, false231}