1import { describe, it } from "@effect/vitest"
2import {
3 assertFalse,
4 assertNone,
5 assertSome,
6 assertTrue,
7 deepStrictEqual,
8 strictEqual,
9 throws
10} from "@effect/vitest/utils"
11import { ByteSize, Equal, Hash, Option } from "effect"
12import * as fc from "fast-check"
13
14const arb = fc.bigInt({ min: 0n, max: 10n ** 40n }).map(ByteSize.bytes)
15
16describe("ByteSize", () => {
17 it("decodes strict inputs and validates numeric values at runtime", () => {
18 deepStrictEqual(ByteSize.fromInputUnsafe("4MB"), ByteSize.megabytes(4))
19 assertSome(ByteSize.fromInput("100kB"), ByteSize.kilobytes(100))
20 assertSome(ByteSize.fromInput("2 mebibytes"), ByteSize.mebibytes(2))
21 assertSome(ByteSize.fromInput("0B"), ByteSize.zero)
22 for (const input of [-1, 0.5, NaN, Infinity, Number.MAX_SAFE_INTEGER + 1, -1n]) {
23 assertNone(ByteSize.fromInput(input))
24 throws(() => ByteSize.fromInputUnsafe(input))
25 }
26 })
27
28 it("constructs exact decimal and binary units", () => {
29 const decimal = [
30 ByteSize.kilobytes,
31 ByteSize.megabytes,
32 ByteSize.gigabytes,
33 ByteSize.terabytes,
34 ByteSize.petabytes,
35 ByteSize.exabytes,
36 ByteSize.zettabytes,
37 ByteSize.yottabytes,
38 ByteSize.ronnabytes,
39 ByteSize.quettabytes
40 ]
41 const binary = [
42 ByteSize.kibibytes,
43 ByteSize.mebibytes,
44 ByteSize.gibibytes,
45 ByteSize.tebibytes,
46 ByteSize.pebibytes,
47 ByteSize.exbibytes,
48 ByteSize.zebibytes,
49 ByteSize.yobibytes
50 ]
51 decimal.forEach((constructor, index) => {
52 strictEqual(ByteSize.toBigInt(constructor(1n)), 1_000n ** BigInt(index + 1))
53 })
54 binary.forEach((constructor, index) => {
55 strictEqual(ByteSize.toBigInt(constructor(1n)), 1_024n ** BigInt(index + 1))
56 })
57 })
58
59 it("validates primitive and unit constructor inputs", () => {
60 deepStrictEqual(ByteSize.bytes(-0), ByteSize.zero)
61 deepStrictEqual(ByteSize.bytes(0n), ByteSize.zero)
62 for (const value of [-1, 0.5, Number.MAX_SAFE_INTEGER + 1, NaN, Infinity, -Infinity]) {
63 throws(() => ByteSize.bytes(value))
64 }
65 throws(() => ByteSize.bytes(-1n))
66 deepStrictEqual(ByteSize.kilobytes(1.5), ByteSize.bytes(1500))
67 deepStrictEqual(ByteSize.kibibytes(0.5), ByteSize.bytes(512))
68 throws(() => ByteSize.kibibytes(0.1))
69 })
70
71 it("parses strict SI and IEC strings exactly", () => {
72 const cases: ReadonlyArray<readonly [string, bigint]> = [
73 ["0 B", 0n],
74 ["1 byte", 1n],
75 ["1500 bytes", 1500n],
76 ["1.5 kB", 1500n],
77 ["1.5KiB", 1536n],
78 ["0.001 kB", 1n],
79 ["2 megabytes", 2_000_000n],
80 ["2 mebibytes", 2_097_152n],
81 [" 9007199254740993 B ", 9_007_199_254_740_993n]
82 ]
83 for (const [input, expected] of cases) {
84 deepStrictEqual(ByteSize.fromStringUnsafe(input), ByteSize.bytes(expected))
85 assertSome(ByteSize.fromString(input), ByteSize.bytes(expected))
86 }
87 })
88
89 it("rejects ambiguous, negative, and fractional-byte strings", () => {
90 const invalid = [
91 "",
92 "1",
93 "+1 B",
94 "-1 B",
95 "1e3 B",
96 "1_000 B",
97 "0.5 B",
98 "0.1 KiB",
99 "1 KB",
100 "1 mb",
101 "1 Mb",
102 "1 K",
103 "1 b",
104 "1 MiB trailing"
105 ]
106 for (const input of invalid) {
107 assertNone(ByteSize.fromString(input))
108 throws(() => ByteSize.fromStringUnsafe(input))
109 }
110 })
111
112 it("uses a branded bigint representation", () => {
113 const value = ByteSize.bytes(9_007_199_254_740_993n)
114 strictEqual(typeof value, "bigint")
115 strictEqual(String(value), "9007199254740993")
116 assertTrue(Equal.equals(value, ByteSize.bytes(9_007_199_254_740_993n)))
117 strictEqual(Hash.hash(value), Hash.hash(9_007_199_254_740_993n))
118 assertTrue(ByteSize.isByteSize(value))
119 assertTrue(ByteSize.isByteSize(0n))
120 assertFalse(ByteSize.isByteSize(-1n))
121 })
122
123 it("converts to numbers only within the safe-integer range", () => {
124 assertSome(ByteSize.toNumber(ByteSize.bytes(BigInt(Number.MAX_SAFE_INTEGER))), Number.MAX_SAFE_INTEGER)
125 assertNone(ByteSize.toNumber(ByteSize.bytes(BigInt(Number.MAX_SAFE_INTEGER) + 1n)))
126 throws(() => ByteSize.toNumberUnsafe(ByteSize.bytes(BigInt(Number.MAX_SAFE_INTEGER) + 1n)))
127 strictEqual(ByteSize.toUnit(ByteSize.kibibytes(3), "KiB"), 3)
128 })
129
130 it("supports ordering and checked arithmetic", () => {
131 const one = ByteSize.bytes(1)
132 const two = ByteSize.bytes(2)
133 const three = ByteSize.bytes(3)
134 strictEqual(ByteSize.Order(one, two), -1)
135 strictEqual(ByteSize.Order(two, two), 0)
136 strictEqual(ByteSize.Order(two, one), 1)
137 deepStrictEqual(ByteSize.sum(one, two), three)
138 assertSome(ByteSize.subtract(three, one), two)
139 assertNone(ByteSize.subtract(one, two))
140 throws(() => ByteSize.subtractUnsafe(one, two))
141 assertSome(ByteSize.times(three, 2), ByteSize.bytes(6))
142 assertNone(ByteSize.times(three, -1))
143 assertNone(ByteSize.times(three, 0.5))
144 assertSome(ByteSize.divide(ByteSize.bytes(7), 2), three)
145 assertNone(ByteSize.divide(three, 0))
146 assertNone(ByteSize.divide(three, 0.5))
147 deepStrictEqual(ByteSize.ReducerSum.combineAll([one, two]), three)
148 deepStrictEqual(ByteSize.ReducerSum.combineAll([]), ByteSize.zero)
149 deepStrictEqual(ByteSize.CombinerMin.combine(one, two), one)
150 deepStrictEqual(ByteSize.CombinerMax.combine(one, two), two)
151 })
152
153 it("formats without losing the represented value", () => {
154 strictEqual(ByteSize.format(ByteSize.bytes(0)), "0 B")
155 strictEqual(ByteSize.format(ByteSize.bytes(1536)), "1.5 KiB")
156 strictEqual(ByteSize.format(ByteSize.bytes(1536), { system: "decimal" }), "1.54 kB")
157 strictEqual(ByteSize.format(ByteSize.bytes(1500), { unit: "kB", precision: 3, trailingZeros: true }), "1.500 kB")
158 strictEqual(ByteSize.format(ByteSize.mebibytes(1)), "1 MiB")
159 strictEqual(ByteSize.format(ByteSize.subtractUnsafe(ByteSize.mebibytes(1), ByteSize.bytes(1))), "1 MiB")
160 strictEqual(ByteSize.format(ByteSize.bytes(10n ** 35n), { system: "decimal" }), "100000 QB")
161 throws(() => ByteSize.format(ByteSize.zero, { precision: 21 }))
162 })
163
164 it("roundtrips canonical strings", () => {
165 fc.assert(fc.property(arb, (value) => {
166 deepStrictEqual(ByteSize.fromStringUnsafe(`${value} B`), value)
167 }))
168 })
169
170 it("preserves arithmetic invariants", () => {
171 fc.assert(fc.property(arb, arb, (a, b) => {
172 if (ByteSize.isGreaterThanOrEqualTo(a, b)) {
173 deepStrictEqual(ByteSize.sum(ByteSize.subtractUnsafe(a, b), b), a)
174 } else {
175 assertNone(ByteSize.subtract(a, b))
176 }
177 }))
178 fc.assert(fc.property(arb, fc.bigInt({ min: 1n, max: 1_000n }), (value, divisor) => {
179 const quotient = Option.getOrThrow(ByteSize.divide(value, divisor))
180 const product = Option.getOrThrow(ByteSize.times(quotient, divisor))
181 assertTrue(ByteSize.isLessThanOrEqualTo(product, value))
182 assertTrue(ByteSize.isLessThan(value, ByteSize.sum(product, ByteSize.bytes(divisor))))
183 }))
184 })
185
186 it("is total for arbitrary strings", () => {
187 fc.assert(fc.property(fc.string(), (input) => {
188 ByteSize.fromString(input)
189 }))
190 })
191})