1.6 kB, with units a thousand apart from B to EB, one decimal place by default; Excel downloads keep it as the conditional custom format Sheets would write, which reads back as Size.
- commit
- 8ace0e657f9f366152090f8669c1cad6d1a80f8e
- parent
- e3e1ac95287a989690d1fe13a20746601da48b9c
- author
- Dami <dami.etoile@gmail.com>
- kind
- feat risk score 1.1 of 3, Jev's estimate
| M | docs/ | 2 | +- |
| M | docs/ | 11 | +++++++ |
| M | internal/ | 4 | ++- |
| M | internal/ | 4 | ++ |
| A | internal/ | 49 | ++++++++++++++++++++++++++++++ |
| A | internal/ | 40 | ++++++++++++++++++++++++ |
| M | internal/ | 13 | +++++++- |
| M | internal/ | 1 | + |
| M | internal/ | 1 | + |
| M | internal/ | 1 | + |
| M | internal/ | 3 | +- |
| M | internal/ | 25 | +++++++++++++-- |
@@ -45,7 +45,7 @@ for a blank cell, and error values as their code (`"#DIV/0!"`).
4545 | `set(ref, value)` | Enter a value as if typed: numbers, `True`/`False`, `None` (clears), or text, which is read as typed (`"=A1*2"` is a formula, `"$5"` currency, `"'=x"` text). A range gets `value` in every cell, or a list of rows (`[[1, 2], [3, 4]]`) from its first cell |4646 | `set_formula(ref, formula)` | Enter a formula (the `=` is optional) in the first cell and fill it over the range, relative references adjusting as in a copy |4747 | `clear(ref=None)` | Clear contents, keeping formatting; the selection by default |48-| `number_format(ref, kind, decimals=None, pattern="")` | Format numbers: kind is `auto`, `text`, `number`, `percent`, `scientific`, `accounting`, `financial`, `currency`, `date`, `time`, `datetime`, `duration`, or `custom` with a `pattern` such as `"0.0%"` |48+| `number_format(ref, kind, decimals=None, pattern="")` | Format numbers: kind is `auto`, `text`, `number`, `percent`, `scientific`, `accounting`, `financial`, `currency`, `date`, `time`, `datetime`, `duration`, `size`, or `custom` with a `pattern` such as `"0.0%"` |4949 | `get_number_format(ref)` | The kind of a cell's number format |5050 | `offset(ref, cols=0, rows=0)` | `ref` moved, keeping its size and sheet: `offset("A1", 2, 3)` is `"C4"` |5151 @@ -32,9 +32,20 @@ Format > Number, as Sheets names them:
3232 | Time | 3:59:00 PM |3333 | Date time | 9/26/2026 15:59:00 |3434 | Duration | Elapsed hours, minutes and seconds: 24:01:00 |35+| Size | A count of bytes, as nushell shows file sizes: 1.6 kB |3536 3637 Format > Increase decimal places and Format > Decrease decimal places3738 show one more or one less.39+40+Size isn't one of Sheets' formats; it's what a41+[nushell](../terminal/nushell.md) table's sizes come in as. It shows42+whole bytes below 1000 (`512 B`), and above that the number in the43+largest unit that keeps it under 1000 once rounded, units a thousand44+apart: kB, MB, GB, TB, PB and EB, with one decimal place until you45+change it (`1.6 kB`, `999,999` as `1.0 MB`). Sheets would write the46+first three units as the custom format47+`[<1000]0" B";[<1000000]0.0," kB";0.0,," MB"`, which is what an Excel48+download keeps, and what reads back as Size.3849 A formula left Automatic shows the format of what it reads3950 ([building formulas](../formulas/building.md)).4051 @@ -45,6 +45,8 @@ func excelCode(f sheet.Format) string {
4545 return orDefault(f.Pattern, "[h]:mm:ss")4646 case sheet.FmtCustom:4747 return f.Pattern48+ case sheet.FmtSize:49+ return f.Code()4850 }4951 return ""5052 }@@ -189,7 +191,7 @@ func formatOf(id int, code string) sheet.Format {
189191 knownOnce.Do(func() {190192 knownCodes = map[string]sheet.Format{}191193 for _, k := range []sheet.FormatKind{sheet.FmtAccounting, sheet.FmtFinancial, sheet.FmtScientific,192- sheet.FmtPercent, sheet.FmtCurrency, sheet.FmtNumber} {194+ sheet.FmtPercent, sheet.FmtCurrency, sheet.FmtNumber, sheet.FmtSize} {193195 for d := sheet.MaxDecimals; d >= 0; d-- {194196 f := sheet.Format{Kind: k, Decimals: d}195197 knownCodes[normCode(excelCode(f))] = f@@ -204,7 +204,11 @@ func TestFormatCodes(t *testing.T) {
204204 }205205 }206206 // Every format 012 writes reads back as itself.207+ kinds := []sheet.FormatKind{sheet.FmtSize}207208 for k := sheet.FmtText; k <= sheet.FmtDuration; k++ {209+ kinds = append(kinds, k)210+ }211+ for _, k := range kinds {208212 for d := range 4 {209213 f := sheet.Format{Kind: k}210214 if sheet.Preset(k).Decimals > 0 {@@ -0,0 +1,49 @@
1+package numfmt2+3+import (4+ "math"5+ "strings"6+7+ "github.com/FelineStateMachine/012/internal/locale"8+)9+10+// sizeUnits are the units of the Size format, a thousand times apart,11+// as nushell shows file sizes.12+var sizeUnits = []string{"B", "kB", "MB", "GB", "TB", "PB", "EB"}13+14+// Size shows v bytes as nushell shows a file size: whole bytes below15+// 1000 ("512 B"), and above that dec decimals of the largest unit, a16+// thousand times apart, that keeps the number under 1000 once rounded17+// ("1.6 kB", "1.0 MB"). Negative sizes show a minus sign.18+func Size(v float64, dec int) string { return SizeIn(v, dec, locale.Canonical) }19+20+// SizeIn is Size as shown in loc: 1,6 kB in de-DE.21+func SizeIn(v float64, dec int, loc *locale.Locale) string {22+ sign := ""23+ if v < 0 {24+ sign, v = "-", -v25+ }26+ if math.IsInf(v, 0) || math.IsNaN(v) {27+ return sign + "∞ B"28+ }29+ unit := 030+ for unit < len(sizeUnits)-1 && roundTo(v, 0) >= 1000 {31+ v /= 100032+ unit++33+ }34+ if unit > 0 && roundTo(v, dec) >= 1000 && unit < len(sizeUnits)-1 {35+ v /= 100036+ unit++37+ }38+ pat := "0"39+ if unit > 0 && dec > 0 {40+ pat += "." + strings.Repeat("0", dec)41+ }42+ return sign + FormatIn(v, pat, loc) + " " + sizeUnits[unit]43+}44+45+// roundTo rounds v to dec decimals, halves away from zero.46+func roundTo(v float64, dec int) float64 {47+ p := math.Pow10(dec)48+ return math.Round(v*p) / p49+}@@ -0,0 +1,40 @@
1+package numfmt2+3+import (4+ "testing"5+6+ "github.com/FelineStateMachine/012/internal/locale"7+)8+9+func TestSize(t *testing.T) {10+ for _, tc := range []struct {11+ v float6412+ dec int13+ want string14+ }{15+ {0, 1, "0 B"},16+ {512, 1, "512 B"},17+ {999, 1, "999 B"},18+ {999.6, 1, "1.0 kB"},19+ {1000, 1, "1.0 kB"},20+ {1646, 1, "1.6 kB"},21+ {1646, 0, "2 kB"},22+ {1646, 2, "1.65 kB"},23+ {999_999, 1, "1.0 MB"},24+ {2_000_000, 1, "2.0 MB"},25+ {1.5e9, 1, "1.5 GB"},26+ {1.2e12, 1, "1.2 TB"},27+ {8e18, 1, "8.0 EB"},28+ {1e21, 1, "1000.0 EB"},29+ {-5, 1, "-5 B"},30+ {-1500, 1, "-1.5 kB"},31+ } {32+ if got := Size(tc.v, tc.dec); got != tc.want {33+ t.Errorf("Size(%v, %d) = %q, want %q", tc.v, tc.dec, got, tc.want)34+ }35+ }36+ de, _ := locale.Lookup("de-DE")37+ if got := SizeIn(1646, 1, de); got != "1,6 kB" {38+ t.Errorf("de-DE: %q", got)39+ }40+}@@ -41,7 +41,7 @@ func DisplayIn(v Value, f Format, width int, loc *locale.Locale) (string, Align)
4141 }4242 return strings.Repeat("#", inner), AlignRight4343 }44- s := numfmt.FormatIn(v.Num, f.CodeIn(loc), loc)44+ s := formatNumberIn(v.Num, f, loc)4545 if utf8.RuneCountInString(s) > inner {4646 s = strings.Repeat("#", inner)4747 }@@ -65,7 +65,16 @@ func FormatTextIn(v Value, f Format, loc *locale.Locale) string {
6565 case FmtAuto, FmtText:6666 return numfmt.GeneralIn(v.Num, loc)6767 }68- return numfmt.FormatIn(v.Num, f.CodeIn(loc), loc)68+ return formatNumberIn(v.Num, f, loc)69+}70+71+// formatNumberIn renders a number under a format with a pattern, or72+// the Size format, which picks its unit by the number.73+func formatNumberIn(v float64, f Format, loc *locale.Locale) string {74+ if f.Kind == FmtSize {75+ return numfmt.SizeIn(v, f.Decimals, loc)76+ }77+ return numfmt.FormatIn(v, f.CodeIn(loc), loc)6978 }7079 7180 // FormatValue renders v in width columns with one column of padding, the@@ -32,6 +32,7 @@ var numberFormats = []struct {
3232 {"format.time", "Time", "Time: 3:59:00 PM", sheet.Preset(sheet.FmtTime)},3333 {"format.datetime", "Date time", "Date and time: 9/26/2026 15:59:00", sheet.Preset(sheet.FmtDateTime)},3434 {"format.duration", "Duration", "Elapsed hours, minutes and seconds: 24:01:00", sheet.Preset(sheet.FmtDuration)},35+ {"format.size", "Size", "A count of bytes, as nushell shows file sizes: 1.6 kB", sheet.Preset(sheet.FmtSize)},3536 }3637 3738 // textStyles toggle like Sheets: on for the whole selection unless the@@ -75,6 +75,7 @@ func TestFormatCommandsCoverSheetsMenu(t *testing.T) {
7575 {"format.financial", " (1,450.00) "},7676 {"format.currency_rounded", " -$1,450 "},7777 {"format.duration", "-34800:00:00"}, // uses the padding: nothing to its right78+ {"format.size", " -1.5 kB "},7879 {"format.plain_text", " -1450 "},7980 {"format.automatic", " -1450 "},8081 }@@ -78,7 +78,8 @@ var menuBar = []menuDef{
7878 {cmd: "format.number", title: "Number"}, {cmd: "format.percent", title: "Percent"}, {cmd: "format.scientific", title: "Scientific"}, sep,7979 {cmd: "format.accounting", title: "Accounting"}, {cmd: "format.financial", title: "Financial"},8080 {cmd: "format.currency", title: "Currency"}, {cmd: "format.currency_rounded", title: "Currency rounded"}, sep,81- {cmd: "format.date", title: "Date"}, {cmd: "format.time", title: "Time"}, {cmd: "format.datetime", title: "Date time"}, {cmd: "format.duration", title: "Duration"},81+ {cmd: "format.date", title: "Date"}, {cmd: "format.time", title: "Time"}, {cmd: "format.datetime", title: "Date time"}, {cmd: "format.duration", title: "Duration"}, sep,82+ {cmd: "format.size", title: "Size"},8283 }},8384 {cmd: "format.decimals_more", title: "Increase decimal places"}, {cmd: "format.decimals_less", title: "Decrease decimal places"}, sep,8485 {cmd: "format.bold", title: "Bold"}, {cmd: "format.italic", title: "Italic"}, {cmd: "format.underline", title: "Underline"}, {cmd: "format.strikethrough", title: "Strikethrough"}, sep,@@ -24,10 +24,11 @@ const (
2424 FmtDateTime // 9/26/2026 15:59:002525 FmtDuration // 24:01:002626 FmtCustom // Pattern, e.g. from Increase decimal places on Automatic27+ FmtSize // 1.6 kB: a count of bytes, as nushell shows file sizes2728 )2829 2930 var kindNames = [...]string{"auto", "text", "number", "percent", "scientific", "accounting",30- "financial", "currency", "date", "time", "datetime", "duration", "custom"}31+ "financial", "currency", "date", "time", "datetime", "duration", "custom", "size"}3132 3233 // String returns the kind's name as stored in files.3334 func (k FormatKind) String() string {@@ -50,7 +51,7 @@ func ParseFormatKind(s string) (FormatKind, bool) {
5051 // HasDecimals reports whether the kind takes a number of decimal places.5152 func (k FormatKind) HasDecimals() bool {5253 switch k {53- case FmtNumber, FmtPercent, FmtScientific, FmtAccounting, FmtFinancial, FmtCurrency:54+ case FmtNumber, FmtPercent, FmtScientific, FmtAccounting, FmtFinancial, FmtCurrency, FmtSize:5455 return true5556 }5657 return false@@ -76,8 +77,11 @@ type Format struct {
7677 const MaxDecimals = 157778 7879 // Preset returns kind with Sheets' default decimals: two for the number79-// kinds.80+// kinds, and one for sizes, as nushell shows them.8081 func Preset(k FormatKind) Format {82+ if k == FmtSize {83+ return Format{Kind: k, Decimals: 1}84+ }8185 if k.HasDecimals() {8286 return Format{Kind: k, Decimals: 2}8387 }@@ -118,10 +122,25 @@ func (f Format) Code() string {
118122 return "m/d/yyyy h:mm:ss"119123 case FmtDuration:120124 return "[h]:mm:ss"125+ case FmtSize:126+ return SizeCode(f.Decimals)121127 }122128 return ""123129 }124130 131+// 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.136+func 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+}143+125144 // WithDecimals returns f showing delta more (or fewer) decimal places, as126145 // Sheets' Increase and Decrease decimal places do. v is the value shown,127146 // used when f is Automatic to start from the decimals currently visible.