cat / 012
clone

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

main
012 / internal / nuon / write.go
write.go 6.0K
1package nuon23import (4 "bufio"5 "encoding/hex"6 "io"7 "math"8 "strconv"9 "strings"10 "time"11 "unicode"12 "unicode/utf8"13)1415// String is v as NUON text.16func (v Value) String() string { return string(Append(nil, v)) }1718// Append appends v to b as `to nuon` writes it: ints, floats with a19// point (1.0), sizes in bytes (1646b), durations in nanoseconds20// (90000000000ns), dates in RFC 3339 with their offset, binary as21// 0x[...], strings bare where nushell reads them back as the same22// string and quoted otherwise, and a list of records that share their23// columns in the table form.24func Append(b []byte, v Value) []byte {25 switch v.Kind {26 case Null:27 return append(b, "null"...)28 case Bool:29 return strconv.AppendBool(b, v.Bool)30 case Int:31 return strconv.AppendInt(b, v.Int, 10)32 case Float:33 return appendFloat(b, v.Float)34 case String:35 return appendString(b, v.Str)36 case Filesize:37 return append(strconv.AppendInt(b, v.Int, 10), 'b')38 case Duration:39 return append(strconv.AppendInt(b, v.Int, 10), "ns"...)40 case Date:41 return appendDate(b, v.Time)42 case Binary:43 return append(append(append(b, "0x["...), strings.ToUpper(hex.EncodeToString(v.Bytes))...), ']')44 case List:45 return appendList(b, v.List)46 case Record:47 return appendRecord(b, v.Fields)48 }49 return b50}5152func appendFloat(b []byte, f float64) []byte {53 switch {54 case math.IsNaN(f):55 return append(b, "NaN"...)56 case math.IsInf(f, 1):57 return append(b, "inf"...)58 case math.IsInf(f, -1):59 return append(b, "-inf"...)60 }61 start := len(b)62 b = strconv.AppendFloat(b, f, 'f', -1, 64)63 if !strings.ContainsRune(string(b[start:]), '.') {64 b = append(b, ".0"...)65 }66 return b67}6869// appendDate writes t as nushell does: RFC 3339 with its offset, and70// the fraction of a second in 3, 6 or 9 digits when there is one.71func appendDate(b []byte, t time.Time) []byte {72 layout := "2006-01-02T15:04:05"73 switch ns := t.Nanosecond(); {74 case ns == 0:75 case ns%1e6 == 0:76 layout += ".000"77 case ns%1e3 == 0:78 layout += ".000000"79 default:80 layout += ".000000000"81 }82 return t.AppendFormat(b, layout+"-07:00")83}8485func appendList(b []byte, vs []Value) []byte {86 if cols, ok := tableColumns(vs); ok {87 return appendTable(b, cols, vs)88 }89 b = append(b, '[')90 for i, v := range vs {91 if i > 0 {92 b = append(b, ", "...)93 }94 b = Append(b, v)95 }96 return append(b, ']')97}9899func appendRecord(b []byte, fs []Field) []byte {100 b = append(b, '{')101 for i, f := range fs {102 if i > 0 {103 b = append(b, ", "...)104 }105 b = appendString(b, f.Key)106 b = append(b, ": "...)107 b = Append(b, f.Value)108 }109 return append(b, '}')110}111112// tableColumns reports whether vs is a list of records with the same113// keys in the same order, with the keys, which the table form writes114// once.115func tableColumns(vs []Value) ([]string, bool) {116 if len(vs) == 0 || vs[0].Kind != Record || len(vs[0].Fields) == 0 {117 return nil, false118 }119 first := vs[0].Fields120 for _, v := range vs[1:] {121 if v.Kind != Record || len(v.Fields) != len(first) {122 return nil, false123 }124 for i, f := range v.Fields {125 if f.Key != first[i].Key {126 return nil, false127 }128 }129 }130 cols := make([]string, len(first))131 for i, f := range first {132 cols[i] = f.Key133 }134 return cols, true135}136137func appendTable(b []byte, cols []string, vs []Value) []byte {138 b = appendHeader(b, cols)139 for i, v := range vs {140 if i > 0 {141 b = append(b, ", "...)142 }143 b = append(b, '[')144 for j, f := range v.Fields {145 if j > 0 {146 b = append(b, ", "...)147 }148 b = Append(b, f.Value)149 }150 b = append(b, ']')151 }152 return append(b, ']')153}154155// appendHeader writes the table form's start: [[a, b]; .156func appendHeader(b []byte, cols []string) []byte {157 b = append(b, "[["...)158 for i, c := range cols {159 if i > 0 {160 b = append(b, ", "...)161 }162 b = appendString(b, c)163 }164 return append(b, "]; "...)165}166167// appendString writes s bare when nushell reads the bare word back as168// this string, and otherwise in double quotes.169func appendString(b []byte, s string) []byte {170 if bareSafe(s) {171 return append(b, s...)172 }173 b = append(b, '"')174 for _, r := range s {175 switch {176 case r == '"' || r == '\\':177 b = append(b, '\\', byte(r))178 case r == '\n':179 b = append(b, `\n`...)180 case r == '\r':181 b = append(b, `\r`...)182 case r == '\t':183 b = append(b, `\t`...)184 case r < 0x20 || r == 0x7f:185 b = append(b, `\u{`...)186 b = strconv.AppendInt(b, int64(r), 16)187 b = append(b, '}')188 default:189 b = utf8.AppendRune(b, r)190 }191 }192 return append(b, '"')193}194195// bareSafe reports whether s can be written without quotes: letters,196// digits and _ - . only, starting with a letter or _, and not a word197// that reads as something else (true, inf, 1kb, 2026-01-01).198func bareSafe(s string) bool {199 if s == "" || !utf8.ValidString(s) {200 return false201 }202 for i, r := range s {203 switch {204 case r == '_' || unicode.IsLetter(r):205 case i > 0 && (r == '-' || r == '.' || unicode.IsDigit(r)):206 default:207 return false208 }209 }210 switch strings.ToLower(s) {211 case "true", "false", "null", "inf", "infinity", "nan":212 return false213 }214 v, _ := classify(s)215 return v.Kind == String216}217218// TableWriter writes rows as a table in the table form, a row at a time.219// With no rows it writes an empty list, [], as nushell writes an empty220// table.221type TableWriter struct {222 w *bufio.Writer223 cols []string224 rows int225 buf []byte226}227228// NewTableWriter writes a table of columns cols to w.229func NewTableWriter(w io.Writer, cols []string) *TableWriter {230 return &TableWriter{w: bufio.NewWriter(w), cols: cols}231}232233// Write writes a row: a value for each column, in order.234func (t *TableWriter) Write(row []Value) error {235 b := t.buf[:0]236 if t.rows == 0 {237 b = appendHeader(b, t.cols)238 } else {239 b = append(b, ",\n"...)240 }241 t.rows++242 b = append(b, '[')243 for i, v := range row {244 if i > 0 {245 b = append(b, ", "...)246 }247 b = Append(b, v)248 }249 b = append(b, ']')250 t.buf = b251 _, err := t.w.Write(b)252 return err253}254255// Close ends the table and flushes it.256func (t *TableWriter) Close() error {257 end := "]\n"258 if t.rows == 0 {259 end = "[]\n"260 }261 if _, err := t.w.WriteString(end); err != nil {262 return err263 }264 return t.w.Flush()265}