Last active
July 31, 2025 23:24
-
-
Save felixdorn/8f73be5171ed3a44096e200224e5fab7 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package quantity | |
| import ( | |
| "fmt" | |
| "unsafe" | |
| ) | |
| type Quantity struct { | |
| // Scalar represents the magnitude of the quantity | |
| Scalar uint64 | |
| // Prefix is one of 'k', 'K', 'M', 'G', 'T', 'P', 'E', or 0 if absent. | |
| Prefix byte | |
| // BinaryPrefix indicates whether the prefix should be interpreted as a power of 2 instead of a power of 10. | |
| BinaryPrefix bool | |
| // Unit is a string of letters, it ends after the first '/', or EOF | |
| Unit string | |
| // Over is a string of letters, it ends at EOF | |
| Over string | |
| } | |
| const ( | |
| StateScalar = iota | |
| StateScalarEnd | |
| StatePrefix | |
| StateUnit | |
| StateUnitEnd | |
| StateOver | |
| StateOverEnd | |
| ) | |
| // Parse parses an ASCII string representing a quantity, like "1Gib/s" or "10 Kg" | |
| func Parse(s string) (Quantity, error) { | |
| qty := Quantity{} | |
| var cursor int | |
| length := len(s) | |
| state := StateScalar | |
| var scalarBytes [64]byte | |
| var scalarIdx uint8 | |
| // First alloc (escape) | |
| var unitBytes [32]byte | |
| var unitIdx uint8 | |
| // Second alloc (escape) | |
| var overBytes [32]byte | |
| var overIdx uint8 | |
| cursor: | |
| for { | |
| if cursor >= length { | |
| switch state { | |
| case StateScalar, StateUnit, StateOver: | |
| state += 1 | |
| default: | |
| break cursor | |
| } | |
| } else if isSpace(s[cursor]) { | |
| cursor++ | |
| continue | |
| } | |
| switch state { | |
| case StateScalar: | |
| if scalarIdx == 64 { | |
| return Quantity{}, fmt.Errorf("invalid scalar: too long (over 64 bytes)") | |
| } | |
| // We shouldn't discard this byte! | |
| if !isDigit(s[cursor]) { | |
| state = StateScalarEnd | |
| continue | |
| } | |
| scalarBytes[scalarIdx] = s[cursor] | |
| scalarIdx++ | |
| cursor++ | |
| continue | |
| case StateScalarEnd: | |
| if scalarIdx != 0 { | |
| qty.Scalar = parseValidBase10Uint(scalarBytes[:scalarIdx]) | |
| } else { | |
| // There's no scalar value, so we assume 0. | |
| qty.Scalar = 0 | |
| } | |
| state = StatePrefix | |
| continue | |
| case StatePrefix: | |
| switch s[cursor] { | |
| case 'k', 'K', 'M', 'G', 'T', 'P', 'E': | |
| qty.Prefix = s[cursor] | |
| default: | |
| // We shouldn't discard this byte! | |
| state = StateUnit | |
| continue | |
| } | |
| cursor++ | |
| // We look-ahead to check if the prefix is binary, e.g. 1024 not 1000. | |
| if cursor < length && s[cursor] == 'i' { | |
| qty.BinaryPrefix = true | |
| cursor++ | |
| } | |
| // "ki" is wrong, it's Ki. | |
| if qty.Prefix == 'k' && qty.BinaryPrefix { | |
| qty.Prefix = 0 | |
| cursor -= 2 | |
| } | |
| state = StateUnit | |
| continue | |
| case StateUnit: | |
| if !isLetter(s[cursor]) && s[cursor] != '/' { | |
| return Quantity{}, fmt.Errorf("unexpected character '%c' in position %d, expected a letter or '/'", s[cursor], cursor) | |
| } | |
| if s[cursor] == '/' { | |
| state = StateUnitEnd | |
| cursor++ | |
| continue | |
| } | |
| unitBytes[unitIdx] = s[cursor] | |
| unitIdx++ | |
| cursor++ | |
| continue | |
| case StateUnitEnd: | |
| qty.Unit = unsafe.String(unsafe.SliceData(unitBytes[:unitIdx]), unitIdx) | |
| state = StateOver | |
| continue | |
| case StateOver: | |
| if !isLetter(s[cursor]) { | |
| return Quantity{}, fmt.Errorf("unexpected character '%c' in position %d, expected a letter", s[cursor], cursor) | |
| } | |
| overBytes[overIdx] = s[cursor] | |
| overIdx++ | |
| cursor++ | |
| continue | |
| case StateOverEnd: | |
| qty.Over = unsafe.String(unsafe.SliceData(overBytes[:overIdx]), overIdx) | |
| break cursor | |
| } | |
| return Quantity{}, fmt.Errorf("unexpected character '%c' in position %d", s[cursor], cursor) | |
| } | |
| return qty, nil | |
| } | |
| func isLetter(b byte) bool { | |
| return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') | |
| } | |
| func isSpace(b byte) bool { | |
| switch b { | |
| case '\t', '\n', '\v', '\f', '\r', ' ', 0x85, 0xA0: | |
| return true | |
| } | |
| return false | |
| } | |
| func isDigit(b byte) bool { | |
| return b >= '0' && b <= '9' | |
| } | |
| // parseValidBase10Uint expects b to be less than 64 bytes long | |
| // and contain only digits | |
| func parseValidBase10Uint(b []byte) uint64 { | |
| var n uint64 | |
| for _, c := range b { | |
| n *= 10 | |
| n += uint64(c - '0') | |
| } | |
| return n | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package quantity | |
| import ( | |
| "testing" | |
| ) | |
| func TestParse_Ok(t *testing.T) { | |
| cases := map[string]struct { | |
| In string | |
| Qty Quantity | |
| }{ | |
| "scalar only": { | |
| "4", | |
| Quantity{Scalar: 4}, | |
| }, | |
| "scalar with unit": { | |
| "4bit", | |
| Quantity{Scalar: 4, Unit: "bit"}, | |
| }, | |
| "scalar with prefix": { | |
| "4M", | |
| Quantity{Scalar: 4, Prefix: 'M'}, | |
| }, | |
| "scalar with unit and prefix": { | |
| "4Mb", | |
| Quantity{Scalar: 4, Prefix: 'M', Unit: "b"}, | |
| }, | |
| "scalar with unit and binary prefix": { | |
| "11Tib", | |
| Quantity{Scalar: 11, Prefix: 'T', BinaryPrefix: true, Unit: "b"}, | |
| }, | |
| "scalar with unit and binary prefix over a unit": { | |
| "1Gib/s", | |
| Quantity{Scalar: 1, Prefix: 'G', BinaryPrefix: true, Unit: "b", Over: "s"}, | |
| }, | |
| "with whitespace": { | |
| " 1 0 0 0 G b it / second ", | |
| Quantity{Scalar: 1000, Prefix: 'G', Unit: "bit", Over: "second"}, | |
| }, | |
| } | |
| for name, tt := range cases { | |
| t.Run(name, func(t *testing.T) { | |
| qty, err := Parse(tt.In) | |
| if err != nil { | |
| t.Errorf("unexpected error: %s", err) | |
| } | |
| if qty.Scalar != tt.Qty.Scalar { | |
| t.Errorf("unexpected scalar value, expected `%v`, got `%v`", tt.Qty.Scalar, qty.Scalar) | |
| } | |
| if qty.Prefix != tt.Qty.Prefix { | |
| t.Errorf("unexpected prefix, expected `%v`, got `%v`", tt.Qty.Prefix, qty.Prefix) | |
| } | |
| if qty.BinaryPrefix != tt.Qty.BinaryPrefix { | |
| t.Errorf("unexpected binary prefix, expected `%v`, got `%v`", tt.Qty.BinaryPrefix, qty.BinaryPrefix) | |
| } | |
| if qty.Unit != tt.Qty.Unit { | |
| t.Errorf("unexpected unit, expected `%v`, got `%v`", tt.Qty.Unit, qty.Unit) | |
| } | |
| if qty.Over != tt.Qty.Over { | |
| t.Errorf("unexpected over value, expected `%v`, got `%v`", tt.Qty.Over, qty.Over) | |
| } | |
| }) | |
| } | |
| } | |
| func BenchmarkParse(b *testing.B) { | |
| for i := 0; i < b.N; i++ { | |
| Parse("1Gibit / second") | |
| } | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
MIT LICENSED