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 / nuon / parse.go
parse.go 10.5K
1package nuon23import (4 "bytes"5 "math"6 "strconv"7 "strings"8 "time"9)1011// Parse reads one NUON value from data; anything after it but spaces12// and comments is an error.13func Parse(data []byte) (Value, error) {14 s := newScanner(bytes.NewReader(data))15 if _, ok, err := s.space(false); err != nil || !ok {16 if err == nil {17 err = s.fail("no value")18 }19 return Value{}, err20 }21 v, err := s.value()22 if err != nil {23 return Value{}, err24 }25 if c, ok, err := s.space(false); err != nil || ok {26 if err == nil {27 err = s.fail("unexpected %q after the value", c)28 }29 return Value{}, err30 }31 return v, nil32}3334// value reads the value at the next byte, spaces skipped.35func (s *scanner) value() (Value, error) {36 c, _, err := s.peek()37 if err != nil {38 return Value{}, err39 }40 switch {41 case c == '[':42 return s.list()43 case c == '{':44 return s.record()45 case c == '"' || c == '\'' || c == '`':46 str, err := s.str()47 return StringValue(str), err48 case s.isRaw():49 str, err := s.raw()50 return StringValue(str), err51 case c == '(' || c == '$':52 return Value{}, s.fail("expressions and variables aren't NUON")53 }54 word, err := s.bare(false)55 if err != nil {56 return Value{}, err57 }58 if (word == "0x" || word == "0b" || word == "0o") && s.peekAt(0) == '[' {59 s.notJSON = true60 return s.binary(word[1])61 }62 v, json := classify(word)63 if !json {64 s.notJSON = true65 }66 return v, nil67}6869// nest counts a level of lists and records.70func (s *scanner) nest() error {71 if s.depth++; s.depth > maxDepth {72 return s.fail("nested more than %d deep", maxDepth)73 }74 return nil75}7677// list reads [a, b] or the table form [[a, b]; [1, 2], [3, 4]], which78// is a list of records.79func (s *scanner) list() (Value, error) {80 if err := s.nest(); err != nil {81 return Value{}, err82 }83 defer func() { s.depth-- }()84 s.take() // [85 var items []Value86 for i := 0; ; i++ {87 c, ok, err := s.space(true)88 if err != nil {89 return Value{}, err90 }91 if !ok {92 return Value{}, s.fail("unterminated list")93 }94 if c == ']' {95 s.take()96 return ListValue(items...), nil97 }98 if c == ';' && i == 1 && items[0].Kind == List {99 s.take()100 return s.tableRows(items[0].List)101 }102 v, err := s.value()103 if err != nil {104 return Value{}, err105 }106 items = append(items, v)107 }108}109110// tableRows reads the rows of the table form after the ;, up to the111// closing ], as records.112func (s *scanner) tableRows(header []Value) (Value, error) {113 s.notJSON = true114 cols, err := s.columns(header)115 if err != nil {116 return Value{}, err117 }118 var rows []Value119 for {120 c, ok, err := s.space(true)121 if err != nil {122 return Value{}, err123 }124 if !ok {125 return Value{}, s.fail("unterminated table")126 }127 if c == ']' {128 s.take()129 return ListValue(rows...), nil130 }131 row, err := s.tableRow(cols)132 if err != nil {133 return Value{}, err134 }135 rows = append(rows, RecordValue(row...))136 }137}138139// columns are a table's column names, from the list before the ;.140func (s *scanner) columns(header []Value) ([]string, error) {141 cols := make([]string, len(header))142 for i, h := range header {143 switch h.Kind {144 case String:145 cols[i] = h.Str146 case Int, Float, Bool:147 cols[i] = string(Append(nil, h))148 default:149 return nil, s.fail("a column name must be a string, not a %s", h.Kind)150 }151 }152 return cols, nil153}154155// tableRow reads one row of the table form, [1, 2], as fields named by156// cols.157func (s *scanner) tableRow(cols []string) ([]Field, error) {158 if c, _, _ := s.peek(); c != '[' {159 return nil, s.fail("expected a table row, [...]")160 }161 row, err := s.list()162 if err != nil {163 return nil, err164 }165 if len(row.List) != len(cols) {166 return nil, s.fail("a row of %d values in a table of %d columns", len(row.List), len(cols))167 }168 fields := make([]Field, len(cols))169 for i, v := range row.List {170 fields[i] = Field{Key: cols[i], Value: v}171 }172 return fields, nil173}174175// record reads {a: 1, "b c": 2}.176func (s *scanner) record() (Value, error) {177 if err := s.nest(); err != nil {178 return Value{}, err179 }180 defer func() { s.depth-- }()181 s.take() // {182 fields := []Field{}183 for {184 c, ok, err := s.space(true)185 if err != nil {186 return Value{}, err187 }188 if !ok {189 return Value{}, s.fail("unterminated record")190 }191 if c == '}' {192 s.take()193 return RecordValue(fields...), nil194 }195 key, err := s.key(c)196 if err != nil {197 return Value{}, err198 }199 if _, _, err := s.space(false); err != nil {200 return Value{}, err201 }202 if err := s.expect(':'); err != nil {203 return Value{}, err204 }205 if _, ok, err := s.space(false); err != nil || !ok {206 if err == nil {207 err = s.fail("a key without a value")208 }209 return Value{}, err210 }211 v, err := s.value()212 if err != nil {213 return Value{}, err214 }215 fields = append(fields, Field{Key: key, Value: v})216 }217}218219// key reads a record's key, quoted or bare, starting with c.220func (s *scanner) key(c byte) (string, error) {221 switch {222 case c == '"' || c == '\'' || c == '`':223 return s.str()224 case s.isRaw():225 return s.raw()226 }227 s.notJSON = true228 return s.bare(true)229}230231// binary reads the bytes of 0x[...], 0b[...] or 0o[...] after the232// prefix: digits of the base, with spaces between groups allowed.233func (s *scanner) binary(base byte) (Value, error) {234 s.take() // [235 var digits strings.Builder236 for {237 c, err := s.take()238 if err != nil {239 return Value{}, err240 }241 switch {242 case c == ']':243 b, ok := decodeBinary(digits.String(), base)244 if !ok {245 return Value{}, s.fail("bad binary digits")246 }247 return BinaryValue(b), nil248 case isSpace(c) || c == ',' || c == '_':249 default:250 digits.WriteByte(c)251 }252 }253}254255// decodeBinary turns the digits of a binary literal into bytes: two hex256// digits, eight bits or three octal digits a byte, padded on the left.257func decodeBinary(digits string, base byte) ([]byte, bool) {258 per, bits := 2, 16259 switch base {260 case 'b':261 per, bits = 8, 2262 case 'o':263 per, bits = 3, 8264 }265 if pad := len(digits) % per; pad != 0 {266 digits = strings.Repeat("0", per-pad) + digits267 }268 out := make([]byte, 0, len(digits)/per)269 for i := 0; i < len(digits); i += per {270 n, err := strconv.ParseUint(digits[i:i+per], bits, 16)271 if err != nil || n > 255 {272 return nil, false273 }274 out = append(out, byte(n))275 }276 return out, true277}278279// classify says what a bare word is: a keyword, a number, a size, a280// duration, a date, or else a string. json reports whether JSON has it.281func classify(w string) (v Value, json bool) {282 switch w {283 case "true", "false":284 return BoolValue(w == "true"), true285 case "null":286 return NullValue(), true287 case "inf", "+inf", "infinity", "+infinity":288 return FloatValue(math.Inf(1)), false289 case "-inf", "-infinity":290 return FloatValue(math.Inf(-1)), false291 case "NaN", "-NaN":292 return FloatValue(math.NaN()), false293 }294 if v, ok := parseNumber(w); ok {295 return v, jsonNumber(w)296 }297 if v, ok := parseUnit(w); ok {298 return v, false299 }300 if t, ok := parseDate(w); ok {301 return DateValue(t), false302 }303 return StringValue(w), false304}305306// parseNumber reads an int (42, -7, 1_000, 0x2a, 0o17, 0b101) or a307// float (1.5, .5, 5., 1e3).308func parseNumber(w string) (Value, bool) {309 if len(w) > 2 && w[0] == '0' {310 base := map[byte]int{'x': 16, 'o': 8, 'b': 2}[w[1]]311 if base != 0 {312 n, err := strconv.ParseInt(strings.ReplaceAll(w[2:], "_", ""), base, 64)313 return IntValue(n), err == nil314 }315 }316 if !numeric(w) {317 return Value{}, false318 }319 plain := strings.ReplaceAll(w, "_", "")320 if n, err := strconv.ParseInt(plain, 10, 64); err == nil {321 return IntValue(n), true322 }323 f, err := strconv.ParseFloat(plain, 64)324 if err != nil && !isRange(err) {325 return Value{}, false326 }327 return FloatValue(f), true328}329330func isRange(err error) bool {331 ne, ok := err.(*strconv.NumError)332 return ok && ne.Err == strconv.ErrRange333}334335// numeric reports whether w is written with a number's characters and336// has a digit: a sign, digits, underscores between them, a point and an337// exponent.338func numeric(w string) bool {339 digit := false340 for i := 0; i < len(w); i++ {341 c := w[i]342 switch {343 case c >= '0' && c <= '9':344 digit = true345 case c == '_' && i > 0 && i < len(w)-1:346 case c == '.':347 case c == 'e' || c == 'E':348 if !digit {349 return false350 }351 case c == '+' || c == '-':352 if i > 0 && w[i-1] != 'e' && w[i-1] != 'E' {353 return false354 }355 default:356 return false357 }358 }359 return digit360}361362// jsonNumber reports whether JSON writes a number this way.363func jsonNumber(w string) bool {364 w = strings.TrimPrefix(w, "-")365 if w == "" || w[0] < '0' || w[0] > '9' || strings.Contains(w, "_") {366 return false367 }368 if len(w) > 1 && w[0] == '0' && w[1] != '.' && w[1] != 'e' && w[1] != 'E' {369 return false370 }371 return !strings.HasSuffix(w, ".") && !strings.Contains(w, ".e") && !strings.Contains(w, ".E")372}373374// sizeUnits are the file size units, matched ignoring case.375var sizeUnits = map[string]float64{376 "b": 1, "kb": 1e3, "mb": 1e6, "gb": 1e9, "tb": 1e12, "pb": 1e15, "eb": 1e18,377 "kib": 1 << 10, "mib": 1 << 20, "gib": 1 << 30, "tib": 1 << 40, "pib": 1 << 50, "eib": 1 << 60,378}379380// durationUnits are the duration units, in nanoseconds.381var durationUnits = map[string]float64{382 "ns": 1, "us": 1e3, "µs": 1e3, "ms": 1e6, "sec": 1e9, "min": 60e9, "hr": 3600e9, "day": 86400e9, "wk": 7 * 86400e9,383}384385// parseUnit reads a file size (1.5kb) or a duration (90sec): a number386// and a unit. Fractions of a byte or a nanosecond are dropped, as387// nushell drops them.388func parseUnit(w string) (Value, bool) {389 i := len(w)390 for i > 0 && (w[i-1] >= 'a' && w[i-1] <= 'z' || w[i-1] >= 'A' && w[i-1] <= 'Z' || w[i-1] >= 0x80) {391 i--392 }393 num, unit := w[:i], w[i:]394 if num == "" || unit == "" || !numeric(num) || strings.ContainsAny(num, "eE") {395 return Value{}, false396 }397 f, err := strconv.ParseFloat(strings.ReplaceAll(num, "_", ""), 64)398 if err != nil {399 return Value{}, false400 }401 if m, ok := durationUnits[unit]; ok {402 n, ok := toInt(f * m)403 return Value{Kind: Duration, Int: n}, ok404 }405 if m, ok := sizeUnits[strings.ToLower(unit)]; ok {406 n, ok := toInt(f * m)407 return Value{Kind: Filesize, Int: n}, ok408 }409 return Value{}, false410}411412// toInt truncates f, reporting false when it doesn't fit an int64.413func toInt(f float64) (int64, bool) {414 if math.IsNaN(f) || f >= math.MaxInt64 || f < math.MinInt64 {415 return 0, false416 }417 return int64(f), true418}419420// dateLayouts are the forms of date nushell reads; those without an421// offset are in UTC, as nushell reads them.422var dateLayouts = []string{423 time.RFC3339Nano,424 "2006-01-02T15:04:05.999999999",425 "2006-01-02T15:04",426 "2006-01-02",427}428429func parseDate(w string) (time.Time, bool) {430 if len(w) < 10 || w[4] != '-' || w[0] < '0' || w[0] > '9' {431 return time.Time{}, false432 }433 for _, layout := range dateLayouts {434 if t, err := time.Parse(layout, w); err == nil {435 return t, true436 }437 }438 return time.Time{}, false439}