cat / 012
clone

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

main
012 / internal / fileio / nuon.go
nuon.go 5.5K
1package fileio23import (4 "context"5 "errors"6 "io"7 "math"8 "os"9 "time"1011 "src.rest/cat/012/internal/numfmt"12 "src.rest/cat/012/internal/nuon"13 "src.rest/cat/012/internal/sheet"14 "src.rest/cat/012/internal/value"15)1617// NUON and JSON tables become cells with their types (see18// docs/nushell/types.md for the mapping): the header row holds the19// column names, numbers stay numbers, file sizes are bytes in the Size20// format, durations are elapsed time in the Duration format, dates are21// date-time serials in the local time zone, and nested records and22// lists are their NUON text. Rows are read and stored one at a time23// (nuon.Reader), so a table streaming in is stored as it arrives.2425// zone is the time zone dates are shown in: the local one, which tests26// replace.27var zone = func() *time.Location { return time.Local }2829func importNUON(ctx context.Context, name string, opt Options) (*Result, error) {30 return importTable(ctx, name, opt)31}3233func importJSON(ctx context.Context, name string, opt Options) (*Result, error) {34 return importTable(ctx, name, opt)35}3637func importTable(ctx context.Context, name string, opt Options) (*Result, error) {38 f, err := os.Open(name)39 if err != nil {40 return nil, err41 }42 defer f.Close()43 defer context.AfterFunc(ctx, func() { f.Close() })()44 var size int6445 if st, err := f.Stat(); err == nil && st.Mode().IsRegular() {46 size = st.Size()47 }48 counter := &countingReader{r: f}49 res, _, err := readTable(ctx, counter, opt.MaxCells, func(rows int) {50 opt.Progress.setRows(rows)51 opt.Progress.setFrac(counter.n, size)52 })53 return res, err54}5556// readTable reads a NUON or JSON table into a new sheet a row at a time,57// keeping up to maxCells cells (see newBuilder), and reports whether the58// text was all JSON. progress is called every few hundred rows.59func readTable(ctx context.Context, in io.Reader, maxCells int, progress func(rows int)) (*Result, bool, error) {60 r := nuon.NewReader(in)61 b := newBuilder(ctx, maxCells)62 t := tableCells{b: b, cols: map[string]int{}, zone: zone(), dated: map[int]bool{}}63 row := 064 for ; ; row++ {65 if row%256 == 0 {66 if err := ctx.Err(); err != nil {67 return nil, false, err68 }69 progress(row)70 }71 fields, err := r.Next()72 if errors.Is(err, io.EOF) {73 break74 }75 if err != nil {76 return nil, false, err77 }78 if b.isFull() {79 b.fits(sheet.Addr{Row: row + 1}) // counted for the note80 continue81 }82 t.header(r.Header())83 t.row(row+1, fields)84 }85 progress(row)86 s, notes := b.finish(nil)87 return &Result{Sheet: s, Rows: row + 1, Notes: notes}, r.JSON(), nil88}8990// tableCells stores a table's rows: each column's name in the header91// row once it's first seen, and each value under its column.92type tableCells struct {93 b *builder94 cols map[string]int95 zone *time.Location96 dated map[int]bool // columns widened for dates97 fixed bool // widths are the sheet's own: dates widen nothing98}99100// dateTimeWidth fits a date and time in the Date time format, in any101// locale's order: 9/27/2026 11:27:31, 27.09.2026 11:27:31.102const dateTimeWidth = 20103104// header adds the table form's columns, in their order, before any row105// names them.106func (t *tableCells) header(cols []string) {107 for _, c := range cols {108 t.col(c)109 }110}111112// col is the column named c, added after the others if it's new.113func (t *tableCells) col(c string) int {114 i, ok := t.cols[c]115 if !ok {116 i = len(t.cols)117 t.cols[c] = i118 t.b.text(sheet.Addr{Col: i}, c, sheet.Format{}, sheet.Style{})119 }120 return i121}122123func (t *tableCells) row(r int, fields nuon.Row) {124 for _, f := range fields {125 t.value(sheet.Addr{Col: t.col(f.Key), Row: r}, f.Value)126 }127}128129// Durations are elapsed time: a count of days, shown as hours, minutes130// and seconds, with milliseconds when there's less than a second in it.131var (132 durationFormat = sheet.Format{Kind: sheet.FmtDuration}133 durationFormatFrac = sheet.Format{Kind: sheet.FmtDuration, Pattern: "[h]:mm:ss.000"}134)135136// nsPerDay is a day in nanoseconds, a date serial's unit.137const nsPerDay = 86400e9138139// value stores one value as a cell.140func (t *tableCells) value(a sheet.Addr, v nuon.Value) {141 b, none := t.b, sheet.Style{}142 c := nuonCell(v, t.zone)143 switch c.V.Kind {144 case sheet.Empty:145 case sheet.Bool:146 b.boolean(a, c.V.Num != 0, none)147 case sheet.Number:148 b.number(a, c.V.Num, c.F, none)149 default:150 b.text(a, c.V.Str, c.F, none)151 }152 if v.Kind == nuon.Date && !t.fixed && !t.dated[a.Col] && a.Valid() {153 t.dated[a.Col] = true154 b.s.LoadColWidth(a.Col, dateTimeWidth)155 }156}157158// nuonCell is what a cell shows for a NUON value: its value, text as159// the value's own, and its format.160func nuonCell(v nuon.Value, zone *time.Location) sheet.LiveCell {161 num := func(n float64, f sheet.Format) sheet.LiveCell { return sheet.LiveCell{V: value.Num(n), F: f} }162 switch v.Kind {163 case nuon.Null:164 return sheet.LiveCell{}165 case nuon.Bool:166 return sheet.LiveCell{V: value.Boolean(v.Bool)}167 case nuon.Int:168 return num(float64(v.Int), sheet.Format{})169 case nuon.Float:170 if math.IsNaN(v.Float) || math.IsInf(v.Float, 0) {171 return sheet.LiveCell{V: value.Str(v.String())}172 }173 return num(v.Float, sheet.Format{})174 case nuon.String:175 return sheet.LiveCell{V: value.Str(v.Str)}176 case nuon.Filesize:177 return num(float64(v.Int), sheet.Preset(sheet.FmtSize))178 case nuon.Duration:179 f := durationFormat180 if v.Int%1e9 != 0 {181 f = durationFormatFrac182 }183 return num(float64(v.Int)/nsPerDay, f)184 case nuon.Date:185 return num(numfmt.SerialOf(v.Time.In(zone)), sheet.Preset(sheet.FmtDateTime))186 }187 return sheet.LiveCell{V: value.Str(v.String())} // binary, lists and records188}