1package fileio23import (4 "bufio"5 "context"6 "io"7 "math"8 "strings"9 "time"1011 "src.rest/cat/012/internal/nuon"12 "src.rest/cat/012/internal/sheet"13)1415// A table written as NUON or JSON takes its column names from the first16// row of the range and has a record for each row after it. Cells keep17// their types by their formats: numbers in the Size format are file18// sizes, in the Duration format durations, in a date or date-time19// format dates in the local time zone (a time of day alone is a20// duration since midnight); other numbers are ints when whole and21// floats otherwise. Text that is a list, a record or binary as NUON22// writes it goes back as that value, so nested values read from NUON23// return as they came.2425func exportNUON(_ context.Context, name string, snap *Snapshot, _ ExportOptions) (*ExportResult, error) {26 return exportText(name, snap, encodeNUON)27}2829func exportJSON(_ context.Context, name string, snap *Snapshot, _ ExportOptions) (*ExportResult, error) {30 return exportText(name, snap, encodeJSON)31}3233// exportText writes a snapshot to a file with encode.34func exportText(name string, snap *Snapshot, encode func(io.Writer, *Snapshot) (int, error)) (*ExportResult, error) {35 rows := 036 err := writeFile(name, func(w io.Writer) error {37 n, err := encode(w, snap)38 rows = n39 return err40 })41 if err != nil {42 return nil, err43 }44 return &ExportResult{Rows: rows, Notes: formulaNotes(snap)}, nil45}4647// formulaNotes says how many formulas were written as their values.48func formulaNotes(snap *Snapshot) []string {49 formulas := 050 for _, c := range snap.Cells {51 if c.Formula {52 formulas++53 }54 }55 if formulas == 0 {56 return nil57 }58 return []string{count(formulas, "formula", "formulas") + " saved as values"}59}6061// encodeNUON writes the snapshot as a NUON table, returning the rows62// written, the header among them.63func encodeNUON(w io.Writer, snap *Snapshot) (int, error) {64 cols := snapColumns(snap)65 tw := nuon.NewTableWriter(w, cols)66 n := 167 for row := range snapRecords(snap, len(cols)) {68 if err := tw.Write(row); err != nil {69 return 0, err70 }71 n++72 }73 return n, tw.Close()74}7576// encodeJSON writes the snapshot as a JSON list of records, one to a77// line.78func encodeJSON(w io.Writer, snap *Snapshot) (int, error) {79 bw := bufio.NewWriter(w)80 cols := snapColumns(snap)81 fields := make([]nuon.Field, len(cols))82 for i, c := range cols {83 fields[i].Key = c84 }85 n := 186 var buf []byte87 bw.WriteString("[")88 for row := range snapRecords(snap, len(cols)) {89 for i, v := range row {90 fields[i].Value = v91 }92 buf = append(buf[:0], "\n"...)93 if n > 1 {94 buf = append(buf[:0], ",\n"...)95 }96 buf = nuon.AppendJSONRecord(buf, fields)97 if _, err := bw.Write(buf); err != nil {98 return 0, err99 }100 n++101 }102 if n > 1 {103 bw.WriteString("\n")104 }105 bw.WriteString("]\n")106 return n, bw.Flush()107}108109// snapColumns are the names in the first row of the range, as shown; a110// blank one is named after its column letter.111func snapColumns(snap *Snapshot) []string {112 r := snap.Range113 cols := make([]string, r.To.Col-r.From.Col+1)114 for col := r.From.Col; col <= r.To.Col; col++ {115 name := ""116 if c, ok := snap.Cells[sheet.Addr{Col: col, Row: r.From.Row}]; ok {117 name = strings.TrimSpace(c.Text())118 }119 if name == "" {120 name = sheet.ColName(col)121 }122 cols[col-r.From.Col] = name123 }124 return cols125}126127// snapRecords yields the values of each row after the first that the128// filter shows. The slice is reused from row to row.129func snapRecords(snap *Snapshot, n int) func(yield func([]nuon.Value) bool) {130 return func(yield func([]nuon.Value) bool) {131 r, loc := snap.Range, zone()132 row := make([]nuon.Value, n)133 for y := r.From.Row + 1; y <= r.To.Row; y++ {134 if snap.HiddenRows[y] {135 continue136 }137 for x := r.From.Col; x <= r.To.Col; x++ {138 c, ok := snap.Cells[sheet.Addr{Col: x, Row: y}]139 row[x-r.From.Col] = nuon.NullValue()140 if ok {141 row[x-r.From.Col] = nuonValue(c, loc)142 }143 }144 if !yield(row) {145 return146 }147 }148 }149}150151// CellValue is a cell as a NUON value, typed by its format as a table152// written as NUON or JSON types it: what 012 get writes for one cell.153func CellValue(c SnapCell) nuon.Value { return nuonValue(c, zone()) }154155// nuonValue is a cell as a NUON value, typed by its format.156func nuonValue(c SnapCell, loc *time.Location) nuon.Value {157 switch v := c.Value; v.Kind {158 case sheet.Empty:159 return nuon.NullValue()160 case sheet.Bool:161 return nuon.BoolValue(v.Num != 0)162 case sheet.Text:163 return textValue(v.Str)164 case sheet.Number:165 return numberValue(v.Num, c.Format, loc)166 }167 return nuon.StringValue(c.Value.String()) // an error, as it shows168}169170// textValue is text as a string, or as the list, record or binary it171// is written as.172func textValue(s string) nuon.Value {173 if strings.HasPrefix(s, "[") || strings.HasPrefix(s, "{") || strings.HasPrefix(s, "0x[") {174 v, err := nuon.Parse([]byte(s))175 if err == nil && (v.Kind == nuon.List || v.Kind == nuon.Record || v.Kind == nuon.Binary) && v.String() == s {176 return v177 }178 }179 return nuon.StringValue(s)180}181182// maxExact is the largest magnitude below which every whole float64 is183// exact, so it's written as an int.184const maxExact = 1 << 53185186func numberValue(n float64, f sheet.Format, loc *time.Location) nuon.Value {187 switch f.Kind {188 case sheet.FmtSize:189 if b, ok := wholeInt(n); ok {190 return nuon.FilesizeValue(b)191 }192 case sheet.FmtDuration:193 if ns, ok := wholeInt(n * nsPerDay); ok {194 return nuon.DurationValue(time.Duration(ns))195 }196 case sheet.FmtTime:197 if ns, ok := wholeInt(n * nsPerDay); ok && n >= 0 && n < 1 {198 return nuon.DurationValue(time.Duration(ns))199 }200 return nuon.DateValue(serialTime(n, loc))201 case sheet.FmtDate, sheet.FmtDateTime:202 return nuon.DateValue(serialTime(n, loc))203 }204 if n == math.Trunc(n) && math.Abs(n) < maxExact {205 return nuon.IntValue(int64(n))206 }207 return nuon.FloatValue(n)208}209210// wholeInt rounds f to an int64, reporting false when it doesn't fit.211func wholeInt(f float64) (int64, bool) {212 f = math.Round(f)213 if math.IsNaN(f) || f >= math.MaxInt64 || f <= math.MinInt64 {214 return 0, false215 }216 return int64(f), true217}218219// serialTime is the time a date serial stands for in loc, to the220// microsecond: about as fine as a serial's float holds a time today.221func serialTime(v float64, loc *time.Location) time.Time {222 days := math.Floor(v)223 us := math.Round((v - days) * 86400e6)224 if math.Abs(days) > 1e7 {225 days = math.Copysign(1e7, days) // past the year 29,000: as far as a sheet shows226 }227 return time.Date(1899, 12, 30+int(days), 0, 0, 0, int(us)*1e3, loc)228}