1package value23import (4 "strconv"5 "strings"67 "src.rest/cat/012/internal/numfmt"8)910// FormatKind is a number format from Sheets' Format > Number menu.11type FormatKind uint81213const (14 FmtAuto FormatKind = iota // General; formulas may infer a format15 FmtText // Plain text: entries stay as typed16 FmtNumber // 1,000.1217 FmtPercent // 10.12%18 FmtScientific // 1.01E+0319 FmtAccounting // $ (1,000.12)20 FmtFinancial // (1,000.12)21 FmtCurrency // $1,000.1222 FmtDate // 9/26/202623 FmtTime // 3:59:00 PM24 FmtDateTime // 9/26/2026 15:59:0025 FmtDuration // 24:01:0026 FmtCustom // Pattern, e.g. from Increase decimal places on Automatic27 FmtSize // 1.6 kB: a count of bytes, as nushell shows file sizes28)2930var kindNames = [...]string{"auto", "text", "number", "percent", "scientific", "accounting",31 "financial", "currency", "date", "time", "datetime", "duration", "custom", "size"}3233// String returns the kind's name as stored in files.34func (k FormatKind) String() string {35 if int(k) < len(kindNames) {36 return kindNames[k]37 }38 return "auto"39}4041// ParseFormatKind is the inverse of FormatKind.String.42func ParseFormatKind(s string) (FormatKind, bool) {43 for i, n := range kindNames {44 if n == s {45 return FormatKind(i), true46 }47 }48 return FmtAuto, false49}5051// HasDecimals reports whether the kind takes a number of decimal places.52func (k FormatKind) HasDecimals() bool {53 switch k {54 case FmtNumber, FmtPercent, FmtScientific, FmtAccounting, FmtFinancial, FmtCurrency, FmtSize:55 return true56 }57 return false58}5960// IsTime reports whether the kind shows a date, time or duration.61func (k FormatKind) IsTime() bool {62 return k >= FmtDate && k <= FmtDuration63}6465// Format is a cell's number format. It is a plain value so cells can be66// copied freely.67type Format struct {68 Kind FormatKind69 Decimals int // for Number, Percent, Scientific, Accounting, Financial, Currency70 // Pattern is the pattern of a FmtCustom, or overrides the default71 // pattern of a date or time kind (a date typed as 2026-09-26 keeps72 // that style, as in Sheets).73 Pattern string74}7576// MaxDecimals caps Increase decimal places.77const MaxDecimals = 157879// Preset returns kind with Sheets' default decimals: two for the number80// kinds, and one for sizes, as nushell shows them.81func Preset(k FormatKind) Format {82 if k == FmtSize {83 return Format{Kind: k, Decimals: 1}84 }85 if k.HasDecimals() {86 return Format{Kind: k, Decimals: 2}87 }88 return Format{Kind: k}89}9091// IsZero reports whether f is Automatic.92func (f Format) IsZero() bool { return f == Format{} }9394// Code returns the Sheets-style format pattern f renders with, or "" for95// Automatic and Plain text.96func (f Format) Code() string {97 dec := ""98 if f.Decimals > 0 {99 dec = "." + strings.Repeat("0", min(f.Decimals, MaxDecimals))100 }101 if f.Pattern != "" && (f.Kind == FmtCustom || f.Kind.IsTime()) {102 return f.Pattern103 }104 switch f.Kind {105 case FmtNumber:106 return "#,##0" + dec107 case FmtPercent:108 return "0" + dec + "%"109 case FmtScientific:110 return "0" + dec + "E+00"111 case FmtAccounting:112 return `_("$"* #,##0` + dec + `_);_("$"* \(#,##0` + dec + `\);_("$"* "-"??_);_(@_)`113 case FmtFinancial:114 return "#,##0" + dec + ";(#,##0" + dec + ")"115 case FmtCurrency:116 return `"$"#,##0` + dec117 case FmtDate:118 return "m/d/yyyy"119 case FmtTime:120 return "h:mm:ss am/pm"121 case FmtDateTime:122 return "m/d/yyyy h:mm:ss"123 case FmtDuration:124 return "[h]:mm:ss"125 case FmtSize:126 return SizeCode(f.Decimals)127 }128 return ""129}130131// SizeCode is the custom number format closest to the Size format with132// dec decimals, written with conditions as Sheets and Excel take them:133// bytes below 1000, then kB and MB, e.g.134// [<1000]0" B";[<1000000]0.0," kB";0.0,," MB". Size itself goes on to135// GB, TB, PB and EB; files keep this code for other programs.136func SizeCode(dec int) string {137 d := ""138 if dec > 0 {139 d = "." + strings.Repeat("0", min(dec, MaxDecimals))140 }141 return `[<1000]0" B";[<1000000]0` + d + `," kB";0` + d + `,," MB"`142}143144// WithDecimals returns f showing delta more (or fewer) decimal places, as145// Sheets' Increase and Decrease decimal places do. v is the value shown,146// used when f is Automatic to start from the decimals currently visible.147// Formats without decimals (dates, plain text) are returned unchanged.148func (f Format) WithDecimals(delta int, v float64) Format {149 switch {150 case f.Kind.HasDecimals():151 f.Decimals = clampInt(f.Decimals+delta, 0, MaxDecimals)152 case f.Kind == FmtAuto:153 d := clampInt(visibleDecimals(v)+delta, 0, MaxDecimals)154 f = Format{Kind: FmtCustom, Pattern: "0"}155 if d > 0 {156 f.Pattern += "." + strings.Repeat("0", d)157 }158 case f.Kind == FmtCustom:159 f.Pattern = numfmt.AdjustDecimals(f.Pattern, delta)160 }161 return f162}163164// visibleDecimals is how many decimals Automatic shows for v, up to ten.165func visibleDecimals(v float64) int {166 s := strconv.FormatFloat(v, 'f', -1, 64)167 _, frac, ok := strings.Cut(s, ".")168 if !ok {169 return 0170 }171 return min(len(frac), 10)172}173174func clampInt(v, lo, hi int) int { return max(lo, min(v, hi)) }