cat / 012
clone

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

main
012 / internal / value / parse.go
parse.go 3.1K
1package value23import (4 "strconv"5 "strings"6)78// ParseNumber recognizes numbers the way Google Sheets does on entry:9// optional sign, optional leading currency symbol, thousands separators,10// decimals, exponent, and a trailing percent sign ("12%" is 0.12).11func ParseNumber(s string) (float64, bool) {12 v, _, ok := parseNumberFormat(s)13 return v, ok14}1516// ParseValue recognizes everything Sheets turns into a number on entry:17// numbers, currency, percentages, dates and times. It also returns the18// format Sheets applies, e.g. Currency for "$1,200" or Date for19// "9/26/2026"; plain numbers get Automatic.20func ParseValue(s string) (float64, Format, bool) {21 if v, f, ok := parseNumberFormat(s); ok {22 return v, f, true23 }24 return ParseDateTime(s)25}2627func parseNumberFormat(s string) (float64, Format, bool) {28 s = strings.TrimSpace(s)29 if s == "" {30 return 0, Format{}, false31 }32 neg := false33 if s[0] == '-' || s[0] == '+' {34 neg = s[0] == '-'35 s = s[1:]36 }37 cur := strings.HasPrefix(s, "$")38 s = strings.TrimPrefix(s, "$")39 pct := strings.HasSuffix(s, "%")40 s = strings.TrimSuffix(s, "%")41 if s == "" || !isDigit(s[0]) && s[0] != '.' {42 return 0, Format{}, false43 }44 grouped := strings.Contains(s, ",")45 if grouped {46 if !validGrouping(s) {47 return 0, Format{}, false48 }49 s = strings.ReplaceAll(s, ",", "")50 }51 v, err := strconv.ParseFloat(s, 64)52 dot, exp, odd := scanNumber(s)53 if err != nil || odd || strings.EqualFold(s, "inf") {54 return 0, Format{}, false55 }56 if pct {57 // Shift the decimal point rather than divide, so "0.7%" is the58 // double nearest 0.007 and not 0.006999999999999999.59 if p, err := strconv.ParseFloat(s+"e-2", 64); err == nil {60 v = p61 } else {62 v /= 10063 }64 }65 if neg {66 v = -v67 }68 // The format Sheets infers: two decimals if any were typed.69 dec := 070 if dot {71 dec = 272 }73 var f Format74 switch {75 case cur:76 f = Format{Kind: FmtCurrency, Decimals: dec}77 case pct:78 f = Format{Kind: FmtPercent, Decimals: dec}79 case exp:80 f = Preset(FmtScientific)81 case grouped:82 f = Format{Kind: FmtNumber, Decimals: dec}83 }84 return v, f, true85}8687// scanNumber reports whether s has a decimal point, an exponent, and88// what ParseFloat accepts but an entry isn't: hexadecimal, underscores.89// One pass, as every number loaded goes through it.90func scanNumber(s string) (dot, exp, odd bool) {91 for i := 0; i < len(s); i++ {92 switch s[i] {93 case '.':94 dot = true95 case 'e', 'E':96 exp = true97 case 'x', 'X', 'p', 'P', '_':98 odd = true99 }100 }101 return dot, exp, odd102}103104// validGrouping checks thousands separators: groups of three digits105// before the decimal point, and none after it (1.234,5 is text).106func validGrouping(s string) bool {107 intPart, frac, _ := strings.Cut(s, ".")108 if strings.Contains(frac, ",") {109 return false110 }111 groups := strings.Split(intPart, ",")112 if len(groups[0]) == 0 || len(groups[0]) > 3 {113 return false114 }115 for _, g := range groups[1:] {116 if len(g) != 3 {117 return false118 }119 }120 return true121}122123func isLetter(c byte) bool {124 return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')125}126127func isDigit(c byte) bool {128 return c >= '0' && c <= '9'129}