Skip to content

Commit 1789bc7

Browse files
authored
Merge pull request #16 from RaduBerinde/crhumanize
add crhumanize library
2 parents d205a3f + 6df0aa5 commit 1789bc7

File tree

7 files changed

+687
-0
lines changed

7 files changed

+687
-0
lines changed

crhumanize/bytes.go

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
// Copyright 2025 The Cockroach Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
12+
// implied. See the License for the specific language governing
13+
// permissions and limitations under the License.
14+
15+
package crhumanize
16+
17+
import (
18+
"fmt"
19+
"math"
20+
"strconv"
21+
"strings"
22+
)
23+
24+
// Bytes returns an approximate (within 5%) human-readable representations of a
25+
// byte value in IEC units.
26+
//
27+
// Examples: "1.5 MiB", "21 GiB", "3 B".
28+
func Bytes[T Integer](bytes T) SafeString {
29+
if bytes < 0 {
30+
// Note: uint64(-bytes) doesn't work correctly when bytes is the minimum
31+
// value for a smaller type.
32+
return "-" + bytesUint64(-uint64(bytes), false)
33+
}
34+
return bytesUint64(uint64(bytes), false)
35+
}
36+
37+
// BytesCompact returns an approximate (within 5%) human-readable representations of a
38+
// byte value. It is similar to Bytes but omits the space and the "i" in the
39+
// units. The units are still base-1024.
40+
// Examples: "1.5MB", "21GB", "3B".
41+
func BytesCompact[T Integer](bytes T) SafeString {
42+
if bytes < 0 {
43+
return "-" + bytesUint64(-uint64(bytes), true)
44+
}
45+
return bytesUint64(uint64(bytes), true)
46+
}
47+
48+
func bytesUint64(bytes uint64, compact bool) SafeString {
49+
n, scaled := iecUnit(bytes)
50+
digits := 0
51+
if scaled < 10 {
52+
digits = 1
53+
}
54+
if compact {
55+
return SafeString(fmt.Sprintf("%s%sB", Float(scaled, digits), siUnits[n]))
56+
}
57+
return SafeString(fmt.Sprintf("%s %sB", Float(scaled, digits), iecUnits[n]))
58+
}
59+
60+
// BytesExact is similar to Bytes, but the result is exact and can be parsed
61+
// back into the original value. It separates groups of digits in large numbers
62+
// with commas for readability.
63+
//
64+
// It is guaranteed that ParseBytes[T](BytesExact[T](x)) == x for all x.
65+
//
66+
// An example when this should be used instead of Bytes is when we are
67+
// marshaling a configuration value.
68+
//
69+
// Examples: "1,234 KiB", "21,000 GiB", "1,000,000 B".
70+
func BytesExact[T Integer](bytes T) SafeString {
71+
if bytes < 0 {
72+
return "-" + bytesExactUint64(-uint64(bytes))
73+
}
74+
return bytesExactUint64(uint64(bytes))
75+
}
76+
77+
func bytesExactUint64(bytes uint64) SafeString {
78+
i := 0
79+
if bytes != 0 {
80+
for ; i < len(iecUnits)-1 && bytes%1024 == 0; i++ {
81+
bytes /= 1024
82+
}
83+
}
84+
valStr := strconv.FormatUint(bytes, 10)
85+
var buf strings.Builder
86+
buf.Grow(len(valStr)*4/3 + len(iecUnits[i]) + 2)
87+
88+
// Add commas to make the number more readable.
89+
n := 1 + (len(valStr)-1)%3 // length of the first digit group.
90+
buf.WriteString(valStr[:n])
91+
for i := n; i < len(valStr); i += 3 {
92+
buf.WriteByte(',')
93+
buf.WriteString(valStr[i : i+3])
94+
}
95+
buf.WriteByte(' ')
96+
buf.WriteString(iecUnits[i])
97+
buf.WriteByte('B')
98+
99+
return SafeString(buf.String())
100+
}
101+
102+
func ParseBytes[T Integer](s string) (T, error) {
103+
s = strings.TrimSpace(s)
104+
if s == "" {
105+
return 0, fmt.Errorf("cannot parse bytes from %q", s)
106+
}
107+
unsignedType := T(0)-T(1) > T(0)
108+
minusSign := false
109+
unsignedPart := s
110+
if s[0] == '-' {
111+
// Type is unsigned.
112+
if unsignedType {
113+
return T(0), fmt.Errorf("cannot parse non-negative bytes value from %q", s)
114+
}
115+
minusSign = true
116+
unsignedPart = s[1:]
117+
}
118+
val, err := parseBytesUint64(unsignedPart)
119+
if err != nil {
120+
return 0, err
121+
}
122+
123+
// Apply negation and convert to T, checking for numeric overflow.
124+
result, ok := func() (T, bool) {
125+
if minusSign {
126+
if val > -math.MinInt64 {
127+
return T(0), false
128+
}
129+
x := -int64(val)
130+
result := T(x)
131+
if int64(result) != x {
132+
return T(0), false
133+
}
134+
return result, true
135+
}
136+
137+
result := T(val)
138+
if unsignedType {
139+
if uint64(result) != val {
140+
return T(0), false
141+
}
142+
} else if val > math.MaxInt64 || result < 0 || int64(result) != int64(val) {
143+
return T(0), false
144+
}
145+
return result, true
146+
}()
147+
148+
if !ok {
149+
return T(0), fmt.Errorf("cannot parse bytes value from %q (numeric overflow)", s)
150+
}
151+
return result, nil
152+
}
153+
154+
func parseBytesUint64(s string) (uint64, error) {
155+
numStr := s
156+
for i, r := range s {
157+
if (r >= '0' && r <= '9') || r == '.' || r == ',' {
158+
continue
159+
}
160+
numStr = s[:i]
161+
break
162+
}
163+
suffix := strings.TrimSpace(s[len(numStr):])
164+
suffix = strings.ToUpper(suffix)
165+
// Tolerate but don't require ending with B.
166+
suffix = strings.TrimSuffix(suffix, "B")
167+
unitIdx, _, ok := parseUnit(suffix)
168+
if !ok {
169+
return 0, fmt.Errorf("cannot parse bytes from %q", s)
170+
}
171+
scale := uint64(1) << (10 * unitIdx)
172+
numStr = strings.ReplaceAll(numStr, ",", "")
173+
174+
// We want to guarantee exact parsing of integer values, even those too large
175+
// to be accurately represented in a float64.
176+
if !strings.Contains(numStr, ".") {
177+
value, err := strconv.ParseUint(numStr, 10, 64)
178+
if err != nil {
179+
return 0, fmt.Errorf("cannot parse bytes from %q", s)
180+
}
181+
if value != 0 && scale > math.MaxUint64/value {
182+
return 0, fmt.Errorf("cannot parse bytes from %q (numeric overflow)", s)
183+
}
184+
return scale * value, nil
185+
}
186+
value, err := strconv.ParseFloat(numStr, 64)
187+
if err != nil {
188+
return 0, fmt.Errorf("cannot parse bytes from %q", s)
189+
}
190+
value = math.Round(value * float64(scale))
191+
if value > math.MaxUint64 {
192+
return 0, fmt.Errorf("cannot parse bytes from %q (numeric overflow)", s)
193+
}
194+
return uint64(value), nil
195+
}

0 commit comments

Comments
 (0)