sailfish package - github.com/JekaMas/sailfish - Go Packages

sailfish

package module
v1.0.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 15, 2026 License: MIT Imports: 7 Imported by: 0

README

sailfish

sailfish is a fast, unsigned, fixed-decimal package for trading and financial protocols that exchange exact values as strings such as "123.31232".

The numeric state is one scaled integer:

value = units / 10^fractionalDecimalPlaces

Supported unit backends are uint8, uint16, uint32, uint64, and uint256.Int. The common parse, append, compare, and arithmetic paths perform no heap allocations.

Sailfish requires Go 1.26.5 or newer.

The current release is v1.0.4. On the documented Apple M1 Max / Go 1.26.5 benchmark host, common native formatting is 9.8 ns, runtime-scale uint256 parsing is 8.69 ns, and direct uint256 CBOR decode is 4.20 ns. These caller-buffer operations track measured implementation kernels and perform no heap allocations. See BENCHMARKS.md and PERFORMANCE.md for the complete matrix and rejected alternatives.

Single-format policy

main contains one current implementation and one canonical wire format. It does not retain compatibility codecs, legacy decoders, alternate encodings, or compatibility fallback implementations. FixedDecimal CBOR is always the preferred shortest unsigned integer representation, using tag 2 only when a uint256.Int does not fit in uint64. Input in any other representation is rejected instead of being normalized or decoded by an older path.

Optimizations replace the previous implementation after benchmarks and the complete correctness suite pass. They do not add parallel numbered codec versions.

Quick start

Select semantic kind, integer representation, and fractional decimal places explicitly:

codec, err := sailfish.NewFixedDecimalCodec[sailfish.PriceInUint64Units[sailfish.DecimalPlaces5]]()
if err != nil {
	return err
}

price, err := codec.Parse("123.31232")
if err != nil {
	return err
}

delta, err := codec.Parse("0.00001")
if err != nil {
	return err
}

if overflow := price.AddAssign(delta); overflow {
	return sailfish.ErrOverflow
}

request := make([]byte, 0, 32)
request = codec.AppendTo(request, price)
// request == "123.31233"

The type name is the storage contract:

PriceInUint64Units[DecimalPlaces5]
│            │         └─ exactly 5 digits after the decimal point
│            └─────────── raw numeric state is one uint64 scaled integer
└──────────────────────── semantic kind is price

numeric value = raw units / 100000
12_331_232 raw units = 123.31232

For PriceInUint64Units[DecimalPlaces9], the maximum representable value is 18446744073.709551615.

Choosing a type

Choose semantic kind, fractional decimal places, and integer capacity independently. The format type carries all three choices, so a price cannot be passed where an amount is required even when both use the same decimal places and backend.

Typical value Suggested format Why
Small bounded ratio or rate PriceInUint16Units[DecimalPlaces4] Compact units with four exact fractional digits
CEX price or quantity PriceInUint64Units[DecimalPlaces5], AmountInUint64Units[DecimalPlaces8] Native arithmetic with explicit venue precision
Token amount AmountInUint256Units[DecimalPlaces18] Full EVM-width scaled units
Runtime venue metadata Uint256FixedDecimalCodec Fractional decimal places are validated once without storing them per value

Use the narrowest backend whose complete scaled-integer range covers the protocol contract. Fractional decimal places alone do not determine the backend.

Construction patterns

Parse canonical venue text with a cached codec on repeated paths:

type PriceFormat = sailfish.PriceInUint64Units[sailfish.DecimalPlaces5]
type Price = sailfish.FixedDecimal[PriceFormat, uint64]

priceCodec, err := sailfish.NewFixedDecimalCodec[PriceFormat]()
if err != nil {
	return err
}
price, err := priceCodec.Parse("123.31232")
if err != nil {
	return err
}

Construct directly from already-scaled protocol units without a text round-trip:

type AmountFormat = sailfish.AmountInUint32Units[sailfish.DecimalPlaces6]

amount, err := sailfish.NewFixedDecimalFromUnits[AmountFormat](uint32(1_234_567))
if err != nil {
	return err
}
// amount.String() == "1.234567"

Use distinct formats for domain boundaries:

type CEXPrice = sailfish.FixedDecimal[
	sailfish.PriceInUint64Units[sailfish.DecimalPlaces5],
	uint64,
]
type TokenAmount = sailfish.FixedDecimal[
	sailfish.AmountInUint256Units[sailfish.DecimalPlaces18],
	uint256.Int,
]

For fractional decimal places supplied by trusted venue metadata, validate them once and parse into caller-owned storage:

codec, err := sailfish.NewUint256FixedDecimalCodec(18)
if err != nil {
	return err
}
var units uint256.Int
if err := codec.ParseInto("1.250000000000000000", &units); err != "" {
	return err
}

For large numeric batches, keep the raw scaled units contiguous and use the same typed codec at the text boundary. This avoids carrying each FixedDecimal's optional retained-string header through a price or amount kernel:

type PriceFormat = sailfish.PriceInUint64Units[sailfish.DecimalPlaces5]

codec, err := sailfish.NewFixedDecimalCodec[PriceFormat]()
if err != nil {
	return err
}
units, parseErr := codec.ParseUnits("123.31232")
if parseErr != "" {
	return parseErr
}
prices := []uint64{units}

var wire [32]byte
encoded := codec.AppendUnits(wire[:0], prices[0])

Use FixedDecimal where retained canonical text and typed value methods are useful; use []uint64 or []uint256.Int for large numeric-only working sets. Both forms use the same strict parser and canonical formatter.

Decimal places and storage range

Fractional decimal places and integer capacity are independent. A one-decimal-place price can be 25.5 or 1844674407370955161.5; decimal places alone cannot select a safe backend. Choose an explicit backend when a narrower range is part of the contract:

type SmallPriceFormat = sailfish.PriceInUint16Units[sailfish.DecimalPlaces2]
type SmallPrice = sailfish.FixedDecimal[SmallPriceFormat, uint16]

codec, err := sailfish.NewFixedDecimalCodec[SmallPriceFormat]()
if err != nil {
	return err
}
price, err := codec.Parse("655.35")
// codec.MaxIntegerDigits() == 3

The generic format families are:

PriceInUint{8,16,32,64,256}Units[DecimalPlacesN]
AmountInUint{8,16,32,64,256}Units[DecimalPlacesN]

Price and amount formats remain different types even when backend and decimal places match. DecimalPlaces0 through DecimalPlaces20 are provided; custom zero-sized types can represent other supported decimal-place counts.

Backend Maximum units Maximum fractional decimal places Maximum decimal digits
uint8 255 2 3
uint16 65535 4 5
uint32 4294967295 9 10
uint64 18446744073709551615 19 20
uint256.Int 2^256 - 1 77 78

FixedDecimalCodec.MaxIntegerDigits reports the maximum integer-part digit count for a format. It is a capacity description, not a promise that every number with that many digits fits the binary backend.

There is one format API: PriceInUint*Units[DecimalPlacesN] and AmountInUint*Units[DecimalPlacesN]. Cached FixedDecimalCodec operations resolve fractional decimal places once; use a codec on hot paths. Each generic format embeds a concrete backend, so it does not pay generic backend dispatch.

Wide values

The common 18-decimal on-chain amount is explicit:

type AmountFormat = sailfish.AmountInUint256Units[sailfish.DecimalPlaces18]
type Amount = sailfish.FixedDecimal[AmountFormat, uint256.Int]

amountCodec, err := sailfish.NewFixedDecimalCodec[AmountFormat]()
if err != nil {
	return err
}

The format selects semantic kind, fractional decimal places, and unit backend. The sealed unit-provider interface prevents pairing a format with the wrong unit type.

When trusted venue metadata resolves fractional decimal places at runtime, use the concrete Uint256FixedDecimalCodec to avoid generic format dispatch:

codec, err := sailfish.NewUint256FixedDecimalCodec(6)
if err != nil {
	return err
}

var units uint256.Int
if err := codec.ParseInto("123.456789", &units); err != "" {
	return err
}

dst := codec.AppendTo(make([]byte, 0, 32), units)

Uint256FixedDecimalCodec stores the validated fractional decimal places once. It does not attach metadata to each value; callers remain responsible for selecting the codec from canonical venue metadata.

Parsing and ownership

Parsing is strict. Constructors do not trim input and do not accept signs, exponents, missing integer/fraction digits, or excess precision.

codec.Parse(s)        // retains s only when it is already canonical
codec.ParseCompact(s) // never retains s
codec.ParseBytes(b)   // parses bytes directly and never retains them

Non-canonical accepted input is normalized only while constructing the value:

"001.2" with 5 fractional decimal places -> "1.20000"

Use ParseCompact when a short input string may reference a much larger response buffer.

Immutable string representation

FixedDecimal may retain canonical wire text:

representation string

A string header is smaller than a byte-slice header and String can return it without conversion or allocation. A Go string cannot be safely extended or edited in place. Arithmetic therefore updates units and invalidates only the header:

d.units = newUnits
d.representation = ""

This invalidation allocates nothing. Any string returned before the mutation remains immutable and valid. After mutation:

  • AppendTo remains allocation-free when its destination has capacity.
  • String creates one newly owned string.
  • Canonical returns a copy that retains that string once.

There is no mutable lazy cache, so concurrent reads do not race.

Core API

Need API
Parse retained canonical text NewFixedDecimal, FixedDecimalCodec.Parse
Parse without retaining input NewCompactFixedDecimal, NewFixedDecimalFromBytes, codec equivalents
Construct/read scaled units NewFixedDecimalFromUnits, FixedDecimalCodec.FromUnits, Units
Parse/format compact unit batches FixedDecimalCodec.ParseUnits, FixedDecimalCodec.ParseUnitsBytes, FixedDecimalCodec.AppendUnits, FixedDecimalCodec.UnitsLen
Validate and cache a static format NewFixedDecimalCodec, FixedDecimalCodec.FractionalDecimalPlaces, FixedDecimalCodec.MaxIntegerDigits
Replace or inspect value state SetUnits, IsZero, HasRepresentation
Exact encoded lengths Len, CBORLen, codec equivalents
Runtime-scale uint256 text NewUint256FixedDecimalCodec, Parse, ParseBytes, ParseInto, ParseBytesInto, AppendTo
Caller-buffer serialization AppendTo, AppendJSON, AppendText
Caller-buffer CBOR AppendCBOR, FixedDecimalCodec.AppendCBOR, Uint256FixedDecimalCodec.AppendCBOR
Strict CBOR decode UnmarshalCBOR, FixedDecimalCodec.ParseCBOR, Uint256FixedDecimalCodec.ParseCBOR, Uint256FixedDecimalCodec.ParseCBORInto
Positional-array CBOR decode FixedDecimalCodec.ParseCBORFirst, Uint256FixedDecimalCodec.ParseCBORFirst, Uint256FixedDecimalCodec.ParseCBORFirstInto
Owned or retained serialization String, Canonical, MarshalText, MarshalJSON, MarshalCBOR
Same-format ordering Compare, Cmp, Equal, Less methods
Cross-scale/backend ordering package-level Compare
Checked arithmetic Add, Sub, AddAssign, SubAssign
Overflow-style arithmetic AddOverflow, SubUnderflow

Use FixedDecimalCodec[V, U] for repeated work with a compile-time format. Its zero value is valid; NewFixedDecimalCodec additionally validates and caches the format metadata. Use Uint256FixedDecimalCodec when trusted metadata supplies fractional decimal places at runtime. Its Into methods leave the destination unchanged on error. Invalid formats, inputs, and arithmetic return errors or status values; the package does not use panics as an API contract.

Serialization and deserialization

Sailfish exposes owned standard interfaces and caller-buffer APIs. Use the owned forms at ordinary application boundaries and append/prefix-decode forms for MDBX records, network frames, and other hot aggregate codecs.

Format Encode Decode Wire contract
Canonical text AppendText, MarshalText UnmarshalText Exact fixed-scale ASCII decimal
JSON AppendJSON, MarshalJSON UnmarshalJSON Quoted decimal string; bare numbers rejected
CBOR scalar AppendCBOR, MarshalCBOR ParseCBOR, UnmarshalCBOR Preferred unsigned integer or tag-2 bignum
Positional CBOR repeated AppendCBOR repeated ParseCBORFirst FixedDecimal scalars inside a parent toarray record
Text and JSON

JSON values are quoted decimal strings. Bare JSON numbers are rejected. JSON integration and escaped-string decoding use github.com/goccy/go-json; ordinary unescaped decimal strings decode directly from the JSON input.

text, err := price.MarshalText() // []byte("123.31232")
if err != nil {
	return err
}
jsonValue, err := price.MarshalJSON() // []byte("\"123.31232\"")
if err != nil {
	return err
}

var decoded Price
if err := decoded.UnmarshalJSON(jsonValue); err != nil {
	return err
}

AppendText and AppendJSON reuse caller capacity. MarshalText and MarshalJSON return owned slices and therefore allocate their result once. The direct JSON decoder parses ordinary quoted decimals from the input bytes without allocating. Escaped JSON strings take the standards-compliant go-json unescape path before decimal parsing.

For a hot aggregate encoder, reuse caller capacity rather than asking each field for an owned result:

wire := make([]byte, 0, 32)
wire = price.AppendJSON(wire[:0])

MarshalJSON reserves the exact native/retained size. For an unretained uint256.Int, it reserves the bounded maximum and performs the expensive wide decimal split only once.

Compact deterministic CBOR

CBOR stores only the scaled unsigned integer. The decimal scale and semantic kind are compile-time format identity, while retained source text is cache state; none of them is duplicated in storage.

FixedDecimal[PriceInUint64Units[DecimalPlaces5]] units 12331232 -> 1a00bc28e0
FixedDecimal[AmountInUint256Units[DecimalPlaces18]] units 2^64 -> c249010000000000000000

Native values use RFC 8949's shortest unsigned-integer representation. A uint256.Int uses the same representation while it fits in uint64, then tag 2 with a minimal big-endian magnitude. Decode accepts only preferred, definite-length encodings and rejects trailing data, longer integer forms, leading-zero bignums, and values outside the selected unit backend.

Sailfish decimals implement MarshalCBOR and UnmarshalCBOR for github.com/fxamacker/cbor/v2. They remain scalar elements inside a compact parent array:

type Quote struct {
	_ struct{} `cbor:",toarray"`

	Price  sailfish.FixedDecimal[PriceFormat, uint64]
	Amount sailfish.FixedDecimal[AmountFormat, uint256.Int]
}

This encodes as [priceUnits, amountUnits], not nested one-element arrays. Wire sizes are 1-9 bytes for native units and 1-35 bytes for uint256 units, before the enclosing array header.

Cache fxamacker modes for reflective or cold-path aggregate encoding:

enc, err := cbor.CanonicalEncOptions().EncMode()
if err != nil {
	return err
}
dec, err := cbor.DecOptions{}.DecMode()
if err != nil {
	return err
}

raw, err := enc.Marshal(quote)
if err != nil {
	return err
}
var decoded Quote
if err := dec.Unmarshal(raw, &decoded); err != nil {
	return err
}

Use AppendCBOR or the cached codec equivalent when building a hot MDBX value:

dst := make([]byte, 0, 1+2*sailfish.MaxCBORSize)
dst = append(dst, 0x82) // fixed two-field CBOR array
dst = priceCodec.AppendCBOR(dst, price)
dst = amountCodec.AppendCBOR(dst, amount)

Decode decimal fields from a manual positional array without first finding or copying each scalar item:

price, raw, err := priceCodec.ParseCBORFirst(raw)
if err != nil {
	return err
}
amount, raw, err := amountCodec.ParseCBORFirst(raw)
if err != nil {
	return err
}

ParseCBORFirst consumes exactly one preferred deterministic unsigned value and returns the unconsumed suffix. ParseCBOR remains the whole-item API and rejects trailing data. On failure, prefix decoders return no suffix and ParseCBORFirstInto leaves its destination unchanged.

The hot positional path must validate the parent array header and field count at the enclosing-record layer. Sailfish then validates each scalar's preferred encoding, backend range, and complete consumption. There is one current CBOR format: no compatibility decoder, alternate integer form, or legacy fallback.

These append APIs and all direct decode APIs are 0 B/op, 0 allocs/op with a sized caller buffer. MarshalCBOR necessarily allocates one owned result slice. The reflective fxamacker parent marshal also invokes that owned-slice interface for each decimal; use the append path when aggregate encoding must remain allocation-free. Cache fxamacker EncMode and DecMode for generic or cold paths; they are configured codec instances, not interfaces implemented by application values. Reflective toarray decode remains allocation-free for the tested fixed quote shape.

A permanent fourteen-field oracle test builds a 93-byte positional record with cached Sailfish codecs and verifies byte-for-byte equality with deterministic fxamacker cbor:",toarray". The 93-byte result belongs to that synthetic value set; it is neither a fixed record size nor a theoretical minimum. The same schema has a 15-byte structural floor when its symbol is empty and every numeric value is zero. It has no finite format-wide maximum until the enclosing record bounds symbol length.

A separate July 15, 2026 snapshot covers 100 MEXC spot, 100 Hyperliquid spot, and 100 Hyperliquid perpetual markets, ranked by reported 24-hour volume. It uses distinct positive price and quantity observations from venue metadata, context, ticker, and L2 book responses. Each market identity appears once in the fixture; observed values are deduplicated before min/quantile/max selection. For realistic nonzero records, the resulting 14-field wires are 48-78 bytes:

Cohort Quantity case Min p50 p95 Max Mean
MEXC spot min / median / max 55 / 59 / 62 60 / 63 / 64 68 / 71 / 74 75 / 78 / 78 60.96 / 63.66 / 65.92
Hyperliquid spot min / median / max 48 / 48 / 48 56 / 60 / 60 64 / 66 / 68 69 / 71 / 73 57.42 / 60.18 / 60.80
Hyperliquid perps min / median / max 53 / 56 / 57 56 / 60 / 60 64 / 67 / 68 66 / 68 / 69 57.86 / 60.61 / 61.52

The snapshot and its invariant checks live in testdata/market_cbor_samples.json and market_cbor_benchmark_test.go. Its direct encode path is allocation-free; decode allocates only when the parent record must own a symbol string. Sailfish numeric field decode remains allocation-free. See BENCHMARKS.md for source policy and repeated timing results.

Errors

Errors are typed string constants:

type Error string

const ErrSyntax Error = "sailfish: invalid syntax"

They are comparable, allocation-free to return, and work with errors.Is. Sailfish does not expose panic-on-error constructors. Unsupported fractional decimal places and invalid input are returned as errors. A zero FixedDecimalCodec[V, U] derives valid compile-time decimal places; a zero Uint256FixedDecimalCodec represents zero fractional decimal places.

Range model

The complete digit sequence is one scaled integer, so fractional decimal places consume integer range.

Backend Maximum fractional decimal places Raw units
uint8 2 1 byte
uint16 4 2 bytes
uint32 9 4 bytes
uint64 19 8 bytes
uint256.Int 77 32 bytes

On 64-bit systems:

FixedDecimal[..., uint8]        24 bytes
FixedDecimal[..., uint16]       24 bytes
FixedDecimal[..., uint32]       24 bytes
FixedDecimal[..., uint64]       24 bytes
FixedDecimal[..., uint256.Int]  48 bytes
FixedDecimalCodec                1 byte

Narrow native units enforce smaller ranges and reduce standalone/raw unit arrays. They do not reduce the current FixedDecimal struct below 24 bytes because its retained immutable string header and alignment dominate the layout. The incomparability marker is the first zero-sized field so it does not create trailing zero-field padding. Layout tests lock unit and string offsets, struct alignment, and these sizes on 64-bit targets.

Deliberate boundaries

The initial package does not define:

  • signed decimals;
  • implicit truncation or rounding;
  • multiplication or division rounding policy;
  • floating-point conversion;
  • mutable shared caches;
  • a runtime-varying scale carried by every value.

Those are separate financial contracts, not parser conveniences. Custom zero-sized StaticDecimalPlaces types cover compile-time scales beyond DecimalPlaces20.

Performance

These are five-run summaries from a complete make bench execution on Go 1.26.5, darwin/arm64, Apple M1 Max. Microbenchmark numbers are local, not portable guarantees; compare changes on the same host and toolchain.

Parsing and formatting
Operation Time B/op allocs/op
Parse canonical uint64 through FixedDecimalCodec 7.75 ns 0 0
Parse canonical uint256.Int 49.2 ns 0 0
Parse maximum 78-digit uint256.Int 64.6 ns 0 0
Append retained uint64 2.90 ns 0 0
Append formatted uint64 9.8 ns 0 0
Append formatted four-limb uint256.Int 112 ns 0 0
Return retained String 2.12 ns 0 0
Return newly formatted String 27.1 ns 16 1
Width scaling
Dense parse kernel 19 digits 38 digits 57 digits 77 digits
string input 9.56 ns 18.9 ns 28.5 ns 43.3 ns
[]byte input 9.39 ns 18.2 ns 28.0 ns 42.8 ns
Formatted width One limb Two limbs Three limbs Four limbs Maximum
Wide formatting kernel 17.2 ns 46.0 ns 72.9 ns 107 ns 131 ns

Every width-scaling parse and append row above is 0 B/op, 0 allocs/op.

Comparison and arithmetic
Operation Time B/op allocs/op
Same-scale uint64 compare 2.10 ns 0 0
Same-scale uint256.Int compare 6.37 ns 0 0
Cross-scale/backend compare 50.7 ns 0 0
Checked uint64 add-assign 4.38 ns 0 0
Checked uint256.Int add-assign 13.2 ns 0 0
Serialization
Operation Time B/op allocs/op
Append retained / formatted native JSON 4.41 / 14.4 ns 0 0
Append retained / formatted wide JSON 7.31 / 141 ns 0 0
Owned native retained / formatted MarshalJSON 20.1 / 34.9 ns 16 1
Owned wide retained / formatted MarshalJSON 32.4 / 181 ns 96 1
Unmarshal canonical native / wide JSON 14.4 / 78.8 ns 0 0
Unmarshal escaped native JSON 120 ns 40 2
Append native / uint256 CBOR scalar 3.54 / 8.03 ns 0 0
Decode native / uint256 CBOR scalar 8.07 / 12.8 ns 0 0
Runtime-codec uint256 append, one limb / maximum 4.03 / 6.52 ns 0 0
Runtime-codec uint256 decode, one limb / maximum 4.17 / 5.81 ns 0 0
Owned native / uint256 MarshalCBOR 20.2 / 28.3 ns 16 / 32 1
fxamacker two-field toarray marshal / unmarshal 175 / 146 ns 120 / 0 4 / 0
Manual 14-field positional CBOR encode / decode 50.1 / 93.8 ns 0 / 8 0 / 1

The manual record decoder's one allocation owns its parent string field; Sailfish numeric field decoding is allocation-free. Owned String and marshal results allocate by contract. Detailed commands, profiles, and allocation ownership are in BENCHMARKS.md.

For values parsed from canonical venue text, retain the representation with FixedDecimalCodec.Parse; subsequent appends are below 5 ns even for a four-limb value. Use raw-unit formatting for constructed or mutated values, and call Canonical once when the same formatted value will be emitted repeatedly.

The amd64/arm64 SWAR loader uses one narrowly scoped read-only unsafe load; other architectures use the byte-shift loader. The pointer is never retained or used for mutation, and release validation includes cross-builds, race, and checkptr=2. No assembly or runtime CPU-feature dispatch is used: measured short-token latency does not justify their call and maintenance cost.

Algorithms and measured choices

Area Current algorithm Reason
Scale model Zero-sized compile-time format or one-byte Uint256FixedDecimalCodec Static strategies carry no runtime scale; dynamic venue metadata validates scale once
Numeric model One unsigned scaled integer Exact comparison/arithmetic with no floating-point state
Native parsing Pairwise accumulation plus known-point SWAR for exact 8/16-digit shapes Keeps irregular inputs simple while bringing the common 123.31232 parse to 7.75 ns
Wide parsing One or two independent eight-digit SWAR blocks plus a scalar tail Reduced 19-78 digit parse time by roughly 9-19% in the latest round
SWAR loads One read-only unaligned native load on amd64/arm64; byte shifts elsewhere Removes load assembly on release architectures without retaining or mutating input memory
Native formatting Pairwise digits plus selected reverse-SWAR widths and branchless digit count Arithmetic-packed 5-8 and 14-20 digit scaled values improve 5-29%; other widths retain the smaller pair-table path
Wide formatting Base-1e19 chunks using precomputed-reciprocal 2-by-1 division Avoids serial hardware division and reduced two-to-four-limb formatting by roughly 9-24%
Repeated output Retain immutable canonical input or call Canonical once Repeated append becomes a short string copy
JSON Direct quoted append and parse-first unescaped decode Keeps canonical JSON encode/decode allocation-free; escaped input uses the standards-compliant slow path
CBOR Preferred unsigned integer; tag 2 only above uint64 Small deterministic wire with strict decoding
Hot aggregate CBOR Caller-buffer scalar append and positional prefix decode Avoids reflection and owned per-field slices
Type dispatch Concrete backend embedded in each format; cached scale in FixedDecimalCodec Avoids generic backend type switches and repeated metadata work
Errors Pre-boxed typed string constants Comparable errors with zero per-call failure allocation
Numeric batch locality Contiguous raw units with ParseUnits / AppendUnits Avoids scanning optional text-cache state when only numeric units are used
Profile-guided optimization Production CPU profile owned by the consuming main package PGO optimizes the whole executable; Sailfish does not ship a benchmark-trained library profile

Measured alternatives are not retained in production: base-1e9 and base-1e16 wide formatting were slower outside narrow synthetic cases, direct decimal placement across wide chunks was 2-5% slower, applying base-1e8 native formatting to every width regressed protected paths, generated per-scale masks duplicated code, overstore violated caller-buffer ownership, and a 0-99 cache penalized representative misses. See PERFORMANCE.md for the benchmark artifacts and acceptance decisions.

Cache-locality crossover benchmarks on an Apple M1 Max found no meaningful representation difference for sequential scans, but random numeric scans above the measured L2 were 1.6x faster with []uint64 than native FixedDecimal values and 3.7x faster with []uint256.Int than wide FixedDecimal values. These are working-set measurements, not claimed hardware miss rates; the local CLI CPU counter tools exposed no configured cache-miss event. The package therefore keeps the existing minimum FixedDecimal layout and exposes raw-unit boundary methods instead of adding padding, indirection, or another decimal type.

Profile-guided application builds

Sailfish does not ship default.pgo. Go PGO is a whole-program optimization, so a profile captured from this library's benchmarks cannot represent a consumer's market mix, request frequencies, dependencies, or error paths.

Collect CPU profiles from the deployed executable, merge equal-duration samples when needed, then compare the same revision with and without PGO:

go tool pprof -proto profile-a.pprof profile-b.pprof > default.pgo
go build -pgo=off -o app-no-pgo ./cmd/app
go build -pgo=default.pgo -o app-pgo ./cmd/app

A local mixed Sailfish experiment found 5-23% gains on several parse and wide decode paths, but 8-9% regressions on native CBOR and JSON decode. That profile is deliberately not part of the package. See PERFORMANCE.md for the matrix and compiler diagnostics. Consumers should gate PGO on their whole-application CPU/latency, protected minority paths, binary size, build time, and normal correctness suites.

Branch prediction and assembly

Sailfish uses branchless arithmetic selectively, after distribution-level and public-operation measurements. Native decimal width is derived from bits.Len64, a fixed-point binary-to-decimal estimate, and one borrow-bit threshold correction. On arm64, the compiler emits CLZ, MUL, LSR, and SUBS/NGC instead of the former data-dependent comparison tree.

The isolated digit-width kernel improved by 7-29% across mixed-width, fixed-width, and market-shaped inputs. Thirty paired, order-alternated runs of the public formatted-uint64 append path improved from 12.9 ns to 12.6 ns (-1.98%, p=0.000), with 0 B/op and 0 allocs/op. The complete native-format width matrix remained neutral outside that aggregate public-path gain.

The next formatter round adopted an Abseil-style reverse-SWAR conversion only for measured 5-8 and 14-20 digit scaled widths. A rotated width bitset avoids the guards emitted for variable shifts, packed point insertion avoids a temporary copy for eight-digit values, and explicit BCE proofs collapse two range checks into one. The common 123.31232 append improved from 12.6 ns to 9.8 ns (-22.2%); selected widths improved 5-29%, with 0 B/op and 0 allocs/op. Adjacent pair-table widths remain within roughly 0-1.3% of the prior path.

Architecture-specific AVX-512 IFMA/VBMI formatting remains out of scope: the published algorithm cannot be executed on the arm64 release host, while the selected scalar kernels already complete in about 7-14 ns. The portable comparison also includes Go's pair-table formatter, retained for widths where its smaller dependency chain wins.

Two broader branchless changes were rejected. A four-threshold arithmetic CBOR-length calculation was 13-15% slower than the predictable switch, and segmentio/asm/ascii.ValidString validates ASCII rather than Sailfish decimal grammar, adds a second pass, and uses its generic Go path on darwin/arm64. Existing inlined SWAR digit validation remains the better short-token kernel.

Hardware branch-miss rates are not claimed: this host has no Linux perf, and the available Docker VM exposes no CPU PMU. The assembly and timing evidence, rejected candidates, and a PMU-enabled Linux follow-up command are recorded in PERFORMANCE.md.

Validation

make test
make vet
make race
make bench
make fuzz

Tests include exhaustive byte validation, maximum-value boundaries, randomized exact-reference properties, ownership/cache behavior, allocation assertions, external-package API checks, and fuzz targets for both unit backends and JSON.

If this repository is cloned under a parent directory containing an unrelated go.work, use GOWORK=off or the included Makefile.

License

MIT. See LICENSE.

Documentation

Overview

Package sailfish provides fast, unsigned, fixed-decimal values for trading and financial protocols.

A value is stored as one scaled integer:

value = units / 10^fractionalDecimalPlaces

The package supports uint8, uint16, uint32, uint64, and uint256.Int units. Types make semantic kind, unit representation, and fractional decimal places explicit. For example, PriceInUint64Units[DecimalPlaces5] represents a price as uint64 units with exactly five digits after the decimal point. Its hot parse, text/CBOR append, strict CBOR decode, compare, and arithmetic paths are allocation-free when caller-owned output buffers have capacity.

Index

Examples

Constants

View Source
const MaxCBORSize = 35

MaxCBORSize is the maximum preferred CBOR encoding size of one FixedDecimal. It is tag 2, a one-byte length argument, and a 32-byte uint256 magnitude.

Variables

This section is empty.

Functions

func Compare

func Compare[VA FixedDecimalFormat[UA], UA Unit, VB FixedDecimalFormat[UB], UB Unit](
	a FixedDecimal[VA, UA],
	b FixedDecimal[VB, UB],
) int

Compare compares fixed decimals across fractional decimal-place counts and unit backends exactly. It does not rescale either integer, so comparison cannot overflow.

Types

type AmountInUint8Units added in v1.0.4

type AmountInUint8Units[S StaticDecimalPlaces] struct {
	Uint8Units
}

AmountInUint8Units through AmountInUint256Units are the amount-kind equivalents. Price and amount formats remain distinct types even with equal fractional decimal places and the same backend.

func (AmountInUint8Units[S]) FractionalDecimalPlaces added in v1.0.4

func (AmountInUint8Units[S]) FractionalDecimalPlaces() DecimalPlaces

type AmountInUint16Units added in v1.0.4

type AmountInUint16Units[S StaticDecimalPlaces] struct {
	Uint16Units
}

func (AmountInUint16Units[S]) FractionalDecimalPlaces added in v1.0.4

func (AmountInUint16Units[S]) FractionalDecimalPlaces() DecimalPlaces

type AmountInUint32Units added in v1.0.4

type AmountInUint32Units[S StaticDecimalPlaces] struct {
	Uint32Units
}

func (AmountInUint32Units[S]) FractionalDecimalPlaces added in v1.0.4

func (AmountInUint32Units[S]) FractionalDecimalPlaces() DecimalPlaces

type AmountInUint64Units added in v1.0.4

type AmountInUint64Units[S StaticDecimalPlaces] struct {
	Uint64Units
}

func (AmountInUint64Units[S]) FractionalDecimalPlaces added in v1.0.4

func (AmountInUint64Units[S]) FractionalDecimalPlaces() DecimalPlaces

type AmountInUint256Units added in v1.0.4

type AmountInUint256Units[S StaticDecimalPlaces] struct {
	Uint256Units
}

func (AmountInUint256Units[S]) FractionalDecimalPlaces added in v1.0.4

func (AmountInUint256Units[S]) FractionalDecimalPlaces() DecimalPlaces

type DecimalPlaces added in v1.0.4

type DecimalPlaces uint8

DecimalPlaces is the exact number of fractional digits represented after the decimal point. For example, DecimalPlaces(5) means raw units 12_331_232 represent the decimal value 123.31232.

type DecimalPlaces0 added in v1.0.4

type DecimalPlaces0 struct{}

DecimalPlaces0 through DecimalPlaces20 are zero-sized policies that state the exact number of digits represented after the decimal point. Decimal places are independent from the scaled-integer backend: callers choose both the fractional precision and numeric capacity.

func (DecimalPlaces0) FractionalDecimalPlaces added in v1.0.4

func (DecimalPlaces0) FractionalDecimalPlaces() DecimalPlaces

type DecimalPlaces1 added in v1.0.4

type DecimalPlaces1 struct{}

func (DecimalPlaces1) FractionalDecimalPlaces added in v1.0.4

func (DecimalPlaces1) FractionalDecimalPlaces() DecimalPlaces

type DecimalPlaces2 added in v1.0.4

type DecimalPlaces2 struct{}

func (DecimalPlaces2) FractionalDecimalPlaces added in v1.0.4

func (DecimalPlaces2) FractionalDecimalPlaces() DecimalPlaces

type DecimalPlaces3 added in v1.0.4

type DecimalPlaces3 struct{}

func (DecimalPlaces3) FractionalDecimalPlaces added in v1.0.4

func (DecimalPlaces3) FractionalDecimalPlaces() DecimalPlaces

type DecimalPlaces4 added in v1.0.4

type DecimalPlaces4 struct{}

func (DecimalPlaces4) FractionalDecimalPlaces added in v1.0.4

func (DecimalPlaces4) FractionalDecimalPlaces() DecimalPlaces

type DecimalPlaces5 added in v1.0.4

type DecimalPlaces5 struct{}

func (DecimalPlaces5) FractionalDecimalPlaces added in v1.0.4

func (DecimalPlaces5) FractionalDecimalPlaces() DecimalPlaces

type DecimalPlaces6 added in v1.0.4

type DecimalPlaces6 struct{}

func (DecimalPlaces6) FractionalDecimalPlaces added in v1.0.4

func (DecimalPlaces6) FractionalDecimalPlaces() DecimalPlaces

type DecimalPlaces7 added in v1.0.4

type DecimalPlaces7 struct{}

func (DecimalPlaces7) FractionalDecimalPlaces added in v1.0.4

func (DecimalPlaces7) FractionalDecimalPlaces() DecimalPlaces

type DecimalPlaces8 added in v1.0.4

type DecimalPlaces8 struct{}

func (DecimalPlaces8) FractionalDecimalPlaces added in v1.0.4

func (DecimalPlaces8) FractionalDecimalPlaces() DecimalPlaces

type DecimalPlaces9 added in v1.0.4

type DecimalPlaces9 struct{}

func (DecimalPlaces9) FractionalDecimalPlaces added in v1.0.4

func (DecimalPlaces9) FractionalDecimalPlaces() DecimalPlaces

type DecimalPlaces10 added in v1.0.4

type DecimalPlaces10 struct{}

func (DecimalPlaces10) FractionalDecimalPlaces added in v1.0.4

func (DecimalPlaces10) FractionalDecimalPlaces() DecimalPlaces

type DecimalPlaces11 added in v1.0.4

type DecimalPlaces11 struct{}

func (DecimalPlaces11) FractionalDecimalPlaces added in v1.0.4

func (DecimalPlaces11) FractionalDecimalPlaces() DecimalPlaces

type DecimalPlaces12 added in v1.0.4

type DecimalPlaces12 struct{}

func (DecimalPlaces12) FractionalDecimalPlaces added in v1.0.4

func (DecimalPlaces12) FractionalDecimalPlaces() DecimalPlaces

type DecimalPlaces13 added in v1.0.4

type DecimalPlaces13 struct{}

func (DecimalPlaces13) FractionalDecimalPlaces added in v1.0.4

func (DecimalPlaces13) FractionalDecimalPlaces() DecimalPlaces

type DecimalPlaces14 added in v1.0.4

type DecimalPlaces14 struct{}

func (DecimalPlaces14) FractionalDecimalPlaces added in v1.0.4

func (DecimalPlaces14) FractionalDecimalPlaces() DecimalPlaces

type DecimalPlaces15 added in v1.0.4

type DecimalPlaces15 struct{}

func (DecimalPlaces15) FractionalDecimalPlaces added in v1.0.4

func (DecimalPlaces15) FractionalDecimalPlaces() DecimalPlaces

type DecimalPlaces16 added in v1.0.4

type DecimalPlaces16 struct{}

func (DecimalPlaces16) FractionalDecimalPlaces added in v1.0.4

func (DecimalPlaces16) FractionalDecimalPlaces() DecimalPlaces

type DecimalPlaces17 added in v1.0.4

type DecimalPlaces17 struct{}

func (DecimalPlaces17) FractionalDecimalPlaces added in v1.0.4

func (DecimalPlaces17) FractionalDecimalPlaces() DecimalPlaces

type DecimalPlaces18 added in v1.0.4

type DecimalPlaces18 struct{}

func (DecimalPlaces18) FractionalDecimalPlaces added in v1.0.4

func (DecimalPlaces18) FractionalDecimalPlaces() DecimalPlaces

type DecimalPlaces19 added in v1.0.4

type DecimalPlaces19 struct{}

func (DecimalPlaces19) FractionalDecimalPlaces added in v1.0.4

func (DecimalPlaces19) FractionalDecimalPlaces() DecimalPlaces

type DecimalPlaces20 added in v1.0.4

type DecimalPlaces20 struct{}

func (DecimalPlaces20) FractionalDecimalPlaces added in v1.0.4

func (DecimalPlaces20) FractionalDecimalPlaces() DecimalPlaces

type Error

type Error string

Error is an allocation-free, comparable package error.

Exported errors are typed string constants. They work with errors.Is when returned directly or wrapped with fmt.Errorf and %w.

const (
	ErrSyntax                             Error = "sailfish: invalid syntax"
	ErrRange                              Error = "sailfish: value does not fit unit type"
	ErrPrecision                          Error = "sailfish: too many fractional digits"
	ErrUnsupportedFractionalDecimalPlaces Error = "sailfish: fractional decimal places are unsupported by unit type"
	ErrOverflow                           Error = "sailfish: addition overflow"
	ErrUnderflow                          Error = "sailfish: subtraction underflow"
	ErrNilDestination                     Error = "sailfish: nil destination"
	ErrCBORSyntax                         Error = "sailfish: invalid CBOR"
	ErrCBORNonDeterministic               Error = "sailfish: non-deterministic CBOR"
)

func (Error) Error

func (e Error) Error() string

type FixedDecimal added in v1.0.4

type FixedDecimal[V FixedDecimalFormat[U], U Unit] struct {
	// contains filtered or unexported fields
}

FixedDecimal is an unsigned fixed-scale decimal stored as one scaled integer.

Numeric value = units / 10^fractional-decimal-places. For example, FixedDecimal[PriceInUint64Units[DecimalPlaces5], uint64] stores one uint64; raw units 12_331_232 represent the price 123.31232.

representation is optional immutable wire text. Numeric mutation clears the string header; it never edits string bytes. Clearing the header allocates nothing, and strings previously returned by String remain valid.

Example (Serialization)
package main

import (
	"fmt"

	"github.com/JekaMas/sailfish"
	"github.com/fxamacker/cbor/v2"

	json "github.com/goccy/go-json"
	"github.com/holiman/uint256"
)

type examplePriceFormat = sailfish.PriceInUint64Units[sailfish.DecimalPlaces5]
type examplePrice = sailfish.FixedDecimal[examplePriceFormat, uint64]
type exampleAmountFormat = sailfish.AmountInUint256Units[sailfish.DecimalPlaces18]
type exampleAmount = sailfish.FixedDecimal[exampleAmountFormat, uint256.Int]

type exampleQuote struct {
	_ struct{} `cbor:",toarray"`

	Price  examplePrice
	Amount exampleAmount
}

func main() {
	priceCodec, err := sailfish.NewFixedDecimalCodec[examplePriceFormat]()
	if err != nil {
		fmt.Println(err)
		return
	}
	amountCodec, err := sailfish.NewFixedDecimalCodec[exampleAmountFormat]()
	if err != nil {
		fmt.Println(err)
		return
	}
	price, err := priceCodec.Parse("123.31232")
	if err != nil {
		fmt.Println(err)
		return
	}
	amount, err := amountCodec.Parse("1.250000000000000000")
	if err != nil {
		fmt.Println(err)
		return
	}
	quote := exampleQuote{Price: price, Amount: amount}

	jsonRaw, err := json.Marshal(quote.Price)
	if err != nil {
		fmt.Println(err)
		return
	}
	enc, err := cbor.CanonicalEncOptions().EncMode()
	if err != nil {
		fmt.Println(err)
		return
	}
	cborRaw, err := enc.Marshal(quote)
	if err != nil {
		fmt.Println(err)
		return
	}
	dec, err := cbor.DecOptions{}.DecMode()
	if err != nil {
		fmt.Println(err)
		return
	}
	var decoded exampleQuote
	if err := dec.Unmarshal(cborRaw, &decoded); err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println(string(jsonRaw))
	fmt.Println(decoded.Price.String())
	fmt.Println(decoded.Amount.String())
}
Output:
"123.31232"
123.31232
1.250000000000000000

func NewCompactFixedDecimal added in v1.0.4

func NewCompactFixedDecimal[V FixedDecimalFormat[U], U Unit](s string) (FixedDecimal[V, U], error)

NewCompactFixedDecimal parses s without retaining its backing storage.

func NewFixedDecimal added in v1.0.4

func NewFixedDecimal[V FixedDecimalFormat[U], U Unit](s string) (FixedDecimal[V, U], error)

NewFixedDecimal parses s. It retains s only when s is already canonical fixed-decimal text. Parsing is strict: no whitespace, signs, exponent notation, or excess fractional digits are accepted.

func NewFixedDecimalFromBytes added in v1.0.4

func NewFixedDecimalFromBytes[V FixedDecimalFormat[U], U Unit](b []byte) (FixedDecimal[V, U], error)

NewFixedDecimalFromBytes parses b without retaining or converting it.

func NewFixedDecimalFromUnits added in v1.0.4

func NewFixedDecimalFromUnits[V FixedDecimalFormat[U], U Unit](units U) (FixedDecimal[V, U], error)

NewFixedDecimalFromUnits constructs a decimal from already-scaled units.

Example
package main

import (
	"fmt"

	"github.com/JekaMas/sailfish"
)

func main() {
	type AmountFormat = sailfish.AmountInUint32Units[sailfish.DecimalPlaces6]

	amount, err := sailfish.NewFixedDecimalFromUnits[AmountFormat](uint32(1_234_567))
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println(amount.String())
}
Output:
1.234567

func (FixedDecimal[V, U]) Add added in v1.0.4

func (d FixedDecimal[V, U]) Add(other FixedDecimal[V, U]) (FixedDecimal[V, U], error)

func (*FixedDecimal[V, U]) AddAssign added in v1.0.4

func (d *FixedDecimal[V, U]) AddAssign(other FixedDecimal[V, U]) (overflow bool)

AddAssign leaves d unchanged on overflow. A value-changing success clears cached text without allocation; adding zero preserves it.

func (FixedDecimal[V, U]) AddOverflow added in v1.0.4

func (d FixedDecimal[V, U]) AddOverflow(other FixedDecimal[V, U]) (FixedDecimal[V, U], bool)

AddOverflow returns the wrapped sum and reports unit overflow.

func (FixedDecimal[V, U]) AppendCBOR added in v1.0.4

func (d FixedDecimal[V, U]) AppendCBOR(dst []byte) []byte

AppendCBOR appends the preferred deterministic CBOR encoding. It allocates only when dst has insufficient capacity. When FixedDecimal is a field in a cbor:",toarray" struct, the result is a scalar array element rather than a redundant nested one-element array.

func (FixedDecimal[V, U]) AppendJSON added in v1.0.4

func (d FixedDecimal[V, U]) AppendJSON(dst []byte) []byte

AppendJSON appends a quoted JSON decimal string. FixedDecimal text contains only ASCII digits and a decimal point, so no escaping pass is needed.

func (FixedDecimal[V, U]) AppendText added in v1.0.4

func (d FixedDecimal[V, U]) AppendText(dst []byte) ([]byte, error)

AppendText implements the append-style text encoding contract available in current Go versions without requiring a newly owned result slice.

func (FixedDecimal[V, U]) AppendTo added in v1.0.4

func (d FixedDecimal[V, U]) AppendTo(dst []byte) []byte

AppendTo appends canonical fixed-decimal text. It allocates only when dst has insufficient capacity.

func (FixedDecimal[V, U]) CBORLen added in v1.0.4

func (d FixedDecimal[V, U]) CBORLen() int

CBORLen returns the exact size of the preferred CBOR encoding. FixedDecimal is encoded as its scaled unsigned integer. Scale and retained source text are type/cache metadata and are intentionally absent from the wire format.

func (FixedDecimal[V, U]) Canonical added in v1.0.4

func (d FixedDecimal[V, U]) Canonical() FixedDecimal[V, U]

Canonical returns a copy retaining canonical text. It never mutates shared state and is safe to use concurrently with readers of the original value.

func (FixedDecimal[V, U]) Cmp added in v1.0.4

func (d FixedDecimal[V, U]) Cmp(other FixedDecimal[V, U]) int

func (FixedDecimal[V, U]) Compare added in v1.0.4

func (d FixedDecimal[V, U]) Compare(other FixedDecimal[V, U]) int

Compare returns -1, 0, or +1.

func (FixedDecimal[V, U]) Equal added in v1.0.4

func (d FixedDecimal[V, U]) Equal(other FixedDecimal[V, U]) bool

func (FixedDecimal[V, U]) HasRepresentation added in v1.0.4

func (d FixedDecimal[V, U]) HasRepresentation() bool

HasRepresentation reports whether canonical wire text is currently retained.

func (FixedDecimal[V, U]) IsZero added in v1.0.4

func (d FixedDecimal[V, U]) IsZero() bool

func (FixedDecimal[V, U]) Len added in v1.0.4

func (d FixedDecimal[V, U]) Len() int

Len returns the exact canonical text length.

func (FixedDecimal[V, U]) Less added in v1.0.4

func (d FixedDecimal[V, U]) Less(other FixedDecimal[V, U]) bool

func (FixedDecimal[V, U]) MarshalCBOR added in v1.0.4

func (d FixedDecimal[V, U]) MarshalCBOR() ([]byte, error)

MarshalCBOR implements the fxamacker/cbor Marshaler contract. The returned owned slice necessarily allocates once; use AppendCBOR on hot paths.

func (FixedDecimal[V, U]) MarshalJSON added in v1.0.4

func (d FixedDecimal[V, U]) MarshalJSON() ([]byte, error)

func (FixedDecimal[V, U]) MarshalText added in v1.0.4

func (d FixedDecimal[V, U]) MarshalText() ([]byte, error)

func (*FixedDecimal[V, U]) SetUnits added in v1.0.4

func (d *FixedDecimal[V, U]) SetUnits(units U)

SetUnits replaces the scaled integer. A value-changing update invalidates cached text without allocation; setting the same value preserves it.

func (FixedDecimal[V, U]) String added in v1.0.4

func (d FixedDecimal[V, U]) String() string

String returns retained text when available. Otherwise it creates exactly one result string allocation and does not mutate d.

func (FixedDecimal[V, U]) Sub added in v1.0.4

func (d FixedDecimal[V, U]) Sub(other FixedDecimal[V, U]) (FixedDecimal[V, U], error)

func (*FixedDecimal[V, U]) SubAssign added in v1.0.4

func (d *FixedDecimal[V, U]) SubAssign(other FixedDecimal[V, U]) (underflow bool)

SubAssign leaves d unchanged on underflow. A value-changing success clears cached text without allocation; subtracting zero preserves it.

func (FixedDecimal[V, U]) SubUnderflow added in v1.0.4

func (d FixedDecimal[V, U]) SubUnderflow(other FixedDecimal[V, U]) (FixedDecimal[V, U], bool)

SubUnderflow returns the wrapped difference and reports unit underflow.

func (FixedDecimal[V, U]) Units added in v1.0.4

func (d FixedDecimal[V, U]) Units() U

Units returns the scaled integer by value. uint256.Int is an inline four-limb value, so the returned value owns its storage without allocation.

func (*FixedDecimal[V, U]) UnmarshalCBOR added in v1.0.4

func (d *FixedDecimal[V, U]) UnmarshalCBOR(raw []byte) error

UnmarshalCBOR implements the fxamacker/cbor Unmarshaler contract. It accepts only RFC 8949 preferred deterministic unsigned encodings and leaves d unchanged on failure. Successful decode clears retained text because CBOR carries numeric units only.

func (*FixedDecimal[V, U]) UnmarshalJSON added in v1.0.4

func (d *FixedDecimal[V, U]) UnmarshalJSON(data []byte) error

UnmarshalJSON parses ordinary quoted decimals directly without a separate escape scan. go-json handles escaped strings and non-string JSON syntax.

func (*FixedDecimal[V, U]) UnmarshalText added in v1.0.4

func (d *FixedDecimal[V, U]) UnmarshalText(text []byte) error

type FixedDecimalCodec added in v1.0.4

type FixedDecimalCodec[V FixedDecimalFormat[U], U Unit] struct {
	// contains filtered or unexported fields
}

FixedDecimalCodec validates a fixed-decimal format once and carries its fractional decimal places through repeated parse and format operations. It is the preferred hot-loop API. Its zero value derives the decimal places from the compile-time format; NewFixedDecimalCodec validates and caches them.

The one-byte decimalPlacesPlusOne encoding reserves zero for zero-value derivation.

Example (ManualPositionalCBOR)
package main

import (
	"fmt"

	"github.com/JekaMas/sailfish"
	"github.com/holiman/uint256"
)

type examplePriceFormat = sailfish.PriceInUint64Units[sailfish.DecimalPlaces5]

type exampleAmountFormat = sailfish.AmountInUint256Units[sailfish.DecimalPlaces18]

func main() {
	priceCodec, err := sailfish.NewFixedDecimalCodec[examplePriceFormat]()
	if err != nil {
		fmt.Println(err)
		return
	}
	amountCodec, err := sailfish.NewFixedDecimalCodec[exampleAmountFormat]()
	if err != nil {
		fmt.Println(err)
		return
	}
	price := priceCodec.FromUnits(12_331_232)
	var amountUnits uint256.Int
	amountUnits.SetUint64(1_250_000_000_000_000_000)
	amount := amountCodec.FromUnits(amountUnits)

	record := make([]byte, 0, 1+2*sailfish.MaxCBORSize)
	record = append(record, 0x82) // fixed two-field CBOR array
	record = priceCodec.AppendCBOR(record, price)
	record = amountCodec.AppendCBOR(record, amount)

	raw := record[1:]
	decodedPrice, raw, err := priceCodec.ParseCBORFirst(raw)
	if err != nil {
		fmt.Println(err)
		return
	}
	decodedAmount, raw, err := amountCodec.ParseCBORFirst(raw)
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println(decodedPrice.String())
	fmt.Println(decodedAmount.String())
	fmt.Println(len(raw))
}
Output:
123.31232
1.250000000000000000
0
Example (Price)
package main

import (
	"fmt"

	"github.com/JekaMas/sailfish"
)

type examplePriceFormat = sailfish.PriceInUint64Units[sailfish.DecimalPlaces5]

func main() {
	codec, err := sailfish.NewFixedDecimalCodec[examplePriceFormat]()
	if err != nil {
		fmt.Println(err)
		return
	}

	price, err := codec.Parse("123.31232")
	if err != nil {
		fmt.Println(err)
		return
	}
	next, err := price.Add(codec.FromUnits(1))
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println(next.String())
	fmt.Println(next.Units())
}
Output:
123.31233
12331233

func NewFixedDecimalCodec added in v1.0.4

func NewFixedDecimalCodec[V FixedDecimalFormat[U], U Unit]() (FixedDecimalCodec[V, U], error)

NewFixedDecimalCodec validates the format's fractional decimal places and caches them for repeated operations.

func (FixedDecimalCodec[V, U]) AppendCBOR added in v1.0.4

func (c FixedDecimalCodec[V, U]) AppendCBOR(dst []byte, d FixedDecimal[V, U]) []byte

AppendCBOR appends preferred deterministic CBOR after validating the codec.

func (FixedDecimalCodec[V, U]) AppendJSON added in v1.0.4

func (c FixedDecimalCodec[V, U]) AppendJSON(dst []byte, d FixedDecimal[V, U]) []byte

func (FixedDecimalCodec[V, U]) AppendTo added in v1.0.4

func (c FixedDecimalCodec[V, U]) AppendTo(dst []byte, d FixedDecimal[V, U]) []byte

func (FixedDecimalCodec[V, U]) AppendUnits added in v1.0.4

func (c FixedDecimalCodec[V, U]) AppendUnits(dst []byte, units U) []byte

AppendUnits appends canonical fixed-decimal text directly from raw integer units. It allocates only when dst has insufficient capacity.

func (FixedDecimalCodec[V, U]) CBORLen added in v1.0.4

func (c FixedDecimalCodec[V, U]) CBORLen(d FixedDecimal[V, U]) int

CBORLen returns the exact preferred CBOR size after validating the codec.

func (FixedDecimalCodec[V, U]) Canonical added in v1.0.4

func (c FixedDecimalCodec[V, U]) Canonical(d FixedDecimal[V, U]) FixedDecimal[V, U]

func (FixedDecimalCodec[V, U]) FractionalDecimalPlaces added in v1.0.4

func (c FixedDecimalCodec[V, U]) FractionalDecimalPlaces() DecimalPlaces

FractionalDecimalPlaces returns the exact number of digits represented after the decimal point.

func (FixedDecimalCodec[V, U]) FromUnits added in v1.0.4

func (c FixedDecimalCodec[V, U]) FromUnits(units U) FixedDecimal[V, U]

func (FixedDecimalCodec[V, U]) Len added in v1.0.4

func (c FixedDecimalCodec[V, U]) Len(d FixedDecimal[V, U]) int

func (FixedDecimalCodec[V, U]) MaxIntegerDigits added in v1.0.4

func (c FixedDecimalCodec[V, U]) MaxIntegerDigits() int

MaxIntegerDigits reports how many decimal digits can occur before the point in this backend's maximum value for the configured fractional decimal places. It describes capacity independently from fractional precision; it does not imply every value with that many digits fits the binary backend.

func (FixedDecimalCodec[V, U]) Parse added in v1.0.4

func (c FixedDecimalCodec[V, U]) Parse(s string) (FixedDecimal[V, U], error)

Parse retains s only when it is already canonical fixed-decimal text.

func (FixedDecimalCodec[V, U]) ParseBytes added in v1.0.4

func (c FixedDecimalCodec[V, U]) ParseBytes(b []byte) (FixedDecimal[V, U], error)

ParseBytes parses b directly and never retains it.

func (FixedDecimalCodec[V, U]) ParseCBOR added in v1.0.4

func (c FixedDecimalCodec[V, U]) ParseCBOR(raw []byte) (FixedDecimal[V, U], error)

ParseCBOR decodes preferred deterministic CBOR without retaining raw input.

func (FixedDecimalCodec[V, U]) ParseCBORFirst added in v1.0.4

func (c FixedDecimalCodec[V, U]) ParseCBORFirst(raw []byte) (FixedDecimal[V, U], []byte, error)

ParseCBORFirst decodes one preferred deterministic CBOR decimal from the start of raw and returns the unconsumed suffix. It is the typed hot-path decoder for decimal fields inside manually encoded positional arrays. ParseCBOR remains the strict whole-item API.

func (FixedDecimalCodec[V, U]) ParseCompact added in v1.0.4

func (c FixedDecimalCodec[V, U]) ParseCompact(s string) (FixedDecimal[V, U], error)

ParseCompact never retains s.

func (FixedDecimalCodec[V, U]) ParseUnits added in v1.0.4

func (c FixedDecimalCodec[V, U]) ParseUnits(s string) (U, Error)

ParseUnits parses strict fixed-decimal text directly into the selected unit backend. Use it when a numeric batch stores raw units for the smallest cache footprint and does not need FixedDecimal's optional retained representation. Successful and rejected parses allocate no memory.

func (FixedDecimalCodec[V, U]) ParseUnitsBytes added in v1.0.4

func (c FixedDecimalCodec[V, U]) ParseUnitsBytes(b []byte) (U, Error)

ParseUnitsBytes is ParseUnits for byte input. It neither converts nor retains b.

func (FixedDecimalCodec[V, U]) String added in v1.0.4

func (c FixedDecimalCodec[V, U]) String(d FixedDecimal[V, U]) string

func (FixedDecimalCodec[V, U]) UnitsLen added in v1.0.4

func (c FixedDecimalCodec[V, U]) UnitsLen(units U) int

UnitsLen returns the exact canonical text length of raw integer units.

type FixedDecimalFormat added in v1.0.4

type FixedDecimalFormat[U Unit] interface {
	StaticDecimalPlaces
	// contains filtered or unexported methods
}

FixedDecimalFormat binds a semantic decimal kind, exact fractional decimal places, and one scaled-integer backend. Prefer PriceInUint*Units and AmountInUint*Units. A custom format is normally a zero-sized type:

type QuotePriceWith5DecimalPlaces struct{ sailfish.Uint64Units }
func (QuotePriceWith5DecimalPlaces) FractionalDecimalPlaces() sailfish.DecimalPlaces {
	return 5
}

type NativeUnit

type NativeUnit interface {
	comparable
	uint8 | uint16 | uint32 | uint64
}

NativeUnit is the subset backed by Go's native unsigned integer types.

type PriceInUint8Units added in v1.0.4

type PriceInUint8Units[S StaticDecimalPlaces] struct {
	Uint8Units
}

PriceInUint8Units through PriceInUint256Units identify prices represented by one unsigned scaled integer of the named width. The type parameter states the exact fractional decimal places. For example, PriceInUint64Units[DecimalPlaces5] stores uint64 units and represents numeric value as units / 100000. Backend width controls range; it is not inferred from the decimal places.

func (PriceInUint8Units[S]) FractionalDecimalPlaces added in v1.0.4

func (PriceInUint8Units[S]) FractionalDecimalPlaces() DecimalPlaces

type PriceInUint16Units added in v1.0.4

type PriceInUint16Units[S StaticDecimalPlaces] struct {
	Uint16Units
}

func (PriceInUint16Units[S]) FractionalDecimalPlaces added in v1.0.4

func (PriceInUint16Units[S]) FractionalDecimalPlaces() DecimalPlaces

type PriceInUint32Units added in v1.0.4

type PriceInUint32Units[S StaticDecimalPlaces] struct {
	Uint32Units
}

func (PriceInUint32Units[S]) FractionalDecimalPlaces added in v1.0.4

func (PriceInUint32Units[S]) FractionalDecimalPlaces() DecimalPlaces

type PriceInUint64Units added in v1.0.4

type PriceInUint64Units[S StaticDecimalPlaces] struct {
	Uint64Units
}

func (PriceInUint64Units[S]) FractionalDecimalPlaces added in v1.0.4

func (PriceInUint64Units[S]) FractionalDecimalPlaces() DecimalPlaces

type PriceInUint256Units added in v1.0.4

type PriceInUint256Units[S StaticDecimalPlaces] struct {
	Uint256Units
}

func (PriceInUint256Units[S]) FractionalDecimalPlaces added in v1.0.4

func (PriceInUint256Units[S]) FractionalDecimalPlaces() DecimalPlaces

type StaticDecimalPlaces added in v1.0.4

type StaticDecimalPlaces interface {
	FractionalDecimalPlaces() DecimalPlaces
}

StaticDecimalPlaces supplies an exact compile-time count of fractional decimal places. Implement it on a zero-sized value type with a value receiver.

type Uint8Units

type Uint8Units struct{}

Uint8Units, Uint16Units, and Uint32Units are zero-sized unit providers. Embed one in a custom fixed-decimal format, or use the explicit PriceInUint*Units and AmountInUint*Units formats.

type Uint16Units

type Uint16Units struct{}

type Uint32Units

type Uint32Units struct{}

type Uint64Units

type Uint64Units struct{}

Uint64Units is a zero-sized unit provider. Embed it in a format type.

type Uint256FixedDecimalCodec added in v1.0.4

type Uint256FixedDecimalCodec struct {
	// contains filtered or unexported fields
}

Uint256FixedDecimalCodec is the runtime-decimal-places hot-path codec for scaled uint256 units.

Use FixedDecimalCodec with a FixedDecimalFormat when compile-time semantic identity and decimal places are required. Use Uint256FixedDecimalCodec at boundaries where trusted metadata resolves the fractional decimal places at runtime, such as CEX symbol decoding. Its methods avoid generic format dispatch and return Error directly so successful and rejected parses remain allocation-free.

The zero value represents zero fractional decimal places. The number of decimal places is stored directly in one byte so repeated boundary operations do not decode constructor metadata.

Example
package main

import (
	"fmt"

	"github.com/JekaMas/sailfish"
	"github.com/holiman/uint256"
)

func main() {
	codec, err := sailfish.NewUint256FixedDecimalCodec(18)
	if err != nil {
		fmt.Println(err)
		return
	}

	var units uint256.Int
	if parseErr := codec.ParseInto("1.250000000000000000", &units); parseErr != "" {
		fmt.Println(parseErr)
		return
	}

	fmt.Println(string(codec.AppendTo(nil, units)))
}
Output:
1.250000000000000000

func NewUint256FixedDecimalCodec added in v1.0.4

func NewUint256FixedDecimalCodec(
	fractionalDecimalPlaces DecimalPlaces,
) (Uint256FixedDecimalCodec, error)

NewUint256FixedDecimalCodec validates the exact number of fractional decimal places once for repeated uint256 operations.

func (Uint256FixedDecimalCodec) AppendCBOR added in v1.0.4

func (c Uint256FixedDecimalCodec) AppendCBOR(dst []byte, units uint256.Int) []byte

AppendCBOR appends the preferred deterministic CBOR encoding for units. It allocates only when dst has insufficient capacity.

func (Uint256FixedDecimalCodec) AppendTo added in v1.0.4

func (c Uint256FixedDecimalCodec) AppendTo(dst []byte, units uint256.Int) []byte

AppendTo appends canonical fixed-scale text for units. It allocates only when dst has insufficient capacity.

func (Uint256FixedDecimalCodec) CBORLen added in v1.0.4

func (c Uint256FixedDecimalCodec) CBORLen(units uint256.Int) int

CBORLen returns the exact preferred deterministic CBOR size for units.

func (Uint256FixedDecimalCodec) FractionalDecimalPlaces added in v1.0.4

func (c Uint256FixedDecimalCodec) FractionalDecimalPlaces() DecimalPlaces

FractionalDecimalPlaces returns the exact number of digits represented after the decimal point.

func (Uint256FixedDecimalCodec) Len added in v1.0.4

func (c Uint256FixedDecimalCodec) Len(units uint256.Int) int

Len returns the exact canonical text length for units.

func (Uint256FixedDecimalCodec) MaxIntegerDigits added in v1.0.4

func (c Uint256FixedDecimalCodec) MaxIntegerDigits() int

MaxIntegerDigits reports the maximum integer-part digit count for the configured fractional decimal places.

func (Uint256FixedDecimalCodec) Parse added in v1.0.4

func (c Uint256FixedDecimalCodec) Parse(input string) (uint256.Int, Error)

Parse parses a strict non-negative decimal string into scaled units.

func (Uint256FixedDecimalCodec) ParseBytes added in v1.0.4

func (c Uint256FixedDecimalCodec) ParseBytes(input []byte) (uint256.Int, Error)

ParseBytes parses input without converting it to a string.

func (Uint256FixedDecimalCodec) ParseBytesInto added in v1.0.4

func (c Uint256FixedDecimalCodec) ParseBytesInto(input []byte, dst *uint256.Int) Error

ParseBytesInto parses input into dst without converting it to a string. It leaves dst unchanged on failure.

func (Uint256FixedDecimalCodec) ParseCBOR added in v1.0.4

func (c Uint256FixedDecimalCodec) ParseCBOR(raw []byte) (uint256.Int, Error)

ParseCBOR decodes preferred deterministic CBOR into scaled units.

func (Uint256FixedDecimalCodec) ParseCBORFirst added in v1.0.4

func (c Uint256FixedDecimalCodec) ParseCBORFirst(raw []byte) (uint256.Int, []byte, Error)

ParseCBORFirst decodes one preferred deterministic CBOR uint256 from the start of raw and returns the unconsumed suffix. It is intended for manual positional-array decoders that keep aggregate decoding allocation-free.

func (Uint256FixedDecimalCodec) ParseCBORFirstInto added in v1.0.4

func (c Uint256FixedDecimalCodec) ParseCBORFirstInto(raw []byte, dst *uint256.Int) ([]byte, Error)

ParseCBORFirstInto decodes one preferred deterministic CBOR uint256 into dst and returns the unconsumed suffix. It leaves dst unchanged on failure.

func (Uint256FixedDecimalCodec) ParseCBORInto added in v1.0.4

func (c Uint256FixedDecimalCodec) ParseCBORInto(raw []byte, dst *uint256.Int) Error

ParseCBORInto decodes preferred deterministic CBOR into dst. It leaves dst unchanged on failure.

func (Uint256FixedDecimalCodec) ParseInto added in v1.0.4

func (c Uint256FixedDecimalCodec) ParseInto(input string, dst *uint256.Int) Error

ParseInto parses input into dst. It leaves dst unchanged on failure.

type Uint256Units

type Uint256Units struct{}

Uint256Units is a zero-sized unit provider. Embed it in a format type.

type Unit

type Unit interface {
	comparable
	uint8 | uint16 | uint32 | uint64 | uint256.Int
}

Unit is the closed set of scaled-integer storage backends supported by FixedDecimal.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL