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

sailfish

package module
v1.2.0 Latest Latest
Warning

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

Go to latest
Published: Jul 15, 2026 License: MIT Imports: 8 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.2.0. 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.

For a compact, agent-oriented decision and integration contract, see LLMS.md.

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"

Convert already-scaled big.Int or uint256.Int units at integration boundaries without formatting and reparsing:

type AmountFormat = sailfish.AmountInUint256Units[sailfish.DecimalPlaces18]

codec, err := sailfish.NewFixedDecimalCodec[AmountFormat]()
if err != nil {
	return err
}
amount, err := codec.FromU256(onchainUnits)
if err != nil {
	return err
}

// Reuse caller-owned storage to keep repeated wide output allocation-free.
var destination big.Int
if err := amount.ToBigInt(&destination); err != nil {
	return err
}

FromBigInt and FromU256 accept integer units that are already scaled for the format. They do not multiply, divide, normalize, or change fractional decimal places. Inputs are copied by value and never retained. ToU256 returns an inline four-limb value. ToBigInt writes into its destination; reusing a destination with four-word capacity is allocation-free, while a fresh wide big.Int must allocate its owned words once.

Convert exact rational values without text or floating point:

price, err := priceCodec.FromBigRat(big.NewRat(385_351, 3_125))
if err != nil {
	return err
}

var rational big.Rat
var rationalWorkspace sailfish.BigRatWorkspace
if err := price.ToBigRat(&rational, &rationalWorkspace); err != nil {
	return err
}
// price.String() == "123.31232"
// rational.String() == "385351/3125"

FromBigRat accepts only non-negative rational values whose reduced denominator divides 10^fractionalDecimalPlaces; it rejects values that would require rounding. ToBigRat is always exact. math/big.Rat.SetFrac copies and normalizes private integer words, so fractional ToBigRat output has a measured math/big allocation floor even with a reused destination and workspace. Whole-integer decimals bypass rational normalization and remain allocation-free. There is no math/big.Fraq type; the supported rational boundary is math/big.Rat.

Rescale or combine different fixed-decimal formats explicitly:

price2, _ := sailfish.NewFixedDecimal[
	sailfish.PriceInUint64Units[sailfish.DecimalPlaces2],
]("1.20")
delta5, _ := sailfish.NewFixedDecimal[
	sailfish.PriceInUint64Units[sailfish.DecimalPlaces5],
]("0.00003")

sum, err := sailfish.AddAs[
	sailfish.PriceInUint64Units[sailfish.DecimalPlaces5],
](price2, delta5)
// sum.String() == "1.20003"

Rescale, AddAs, and SubAs never round. Downscaling rejects nonzero discarded units with ErrPrecision; upscaling and result construction reject overflow. Same-format Add/Sub remain the smaller and faster path.

Attach chain/token or market identity without coupling it to decimal places:

type Asset struct {
	Chain uint32
	Token string
}

asset := Asset{Chain: 1, Token: "USDC"}
left := sailfish.NewDenominated(asset, price2)
right := sailfish.NewDenominated(asset, delta5)
total, err := sailfish.AddDenominatedAs[
	sailfish.PriceInUint64Units[sailfish.DecimalPlaces5],
](left, right)

Denominated checks identity before compare/add/subtract and preserves it through exact rescaling. The identity can be a token key, chain/token key, or base/quote market key. It is never consulted to infer scale, normalize text, resolve metadata, or serialize a domain record. This makes it suitable as the numeric core inside an application type such as assetscale, while the application remains responsible for tokenlist lookup and venue metadata.

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
Convert integer packages FixedDecimalCodec.FromBigInt, FixedDecimalCodec.FromU256, ToBigInt, ToU256
Convert exact rational values FixedDecimalCodec.FromBigRat, ToBigRat, BigRatWorkspace
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
Exact cross-scale arithmetic Rescale, AddAs, SubAs
Runtime-denominated values Denominated, NewDenominated, RescaleDenominated, AddDenominatedAs, SubDenominatedAs

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
Integer conversions
Operation Time B/op allocs/op
FromBigInt, uint64 backend 3.93 ns 0 0
FromBigInt, uint256.Int backend 8.30 ns 0 0
FromU256, uint64 backend 2.90 ns 0 0
FromU256, uint256.Int backend 7.29 ns 0 0
ToU256, uint64 / uint256.Int 2.19 / 3.00 ns 0 0
ToBigInt, reused uint64 / uint256.Int destination 4.21 / 6.15 ns 0 0
ToBigInt, fresh wide destination 27.1 ns 32 1

The fresh-destination allocation is math/big's required owned four-word backing storage, not a Sailfish conversion allocation. Prefer a reused destination in hot loops.

Rational, rescale, and denomination operations
Operation Typical result B/op allocs/op
FromBigRat, native exact value about 10 ns 0 0
FromBigRat, uint256.Int exact value about 31 ns 0 0
ToBigRat, whole native integer about 11 ns 0 0
ToBigRat, fractional native value about 108 ns 24 3
Native scale-2 to scale-5 Rescale about 14 ns 0 0
Native mixed-scale AddAs about 20 ns 0 0
Same-scale denominated add, compact identity about 4.8 ns 0 0
Mixed-scale denominated add about 21 ns 0 0

The fractional ToBigRat bytes and allocations are owned by math/big.Rat.SetFrac's mandatory copy/GCD normalization. Sailfish's exact input, rescale, add/subtract, and denomination kernels do not allocate.

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
Exact cross-scale arithmetic Native checked multiply/divide for native results; four-limb arithmetic otherwise Avoids routing ordinary CEX prices through uint256 while preserving overflow and precision checks
Rational input Reduced big.Rat numerator/denominator into native or four-limb units Exact-only conversion with zero heap allocation
Rational output Whole-integer fast path; public math/big.Rat.SetFrac otherwise Integer values avoid GCD/allocation; fractional values retain standard-library ownership and normalization
Runtime identity Generic comparable Denominated wrapper Token/market identity is checked without inferring decimal places or adding interface dispatch
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. FixedDecimalCodec converts already-scaled big.Int and uint256.Int units without decimal rescaling; ToBigInt accepts caller-owned storage so repeated wide conversions can also remain allocation-free. Exact BigRat conversion, explicit cross-scale Rescale/AddAs/SubAs operations, and denomination-aware arithmetic are available without implicit rounding or scale inference.

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 BigRatWorkspace added in v1.2.0

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

BigRatWorkspace owns temporary integer words used by ToBigRat. Its zero value is ready for use. Reuse one workspace and destination per goroutine; neither type is safe for concurrent mutation.

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 Denominated added in v1.2.0

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

Denominated binds a fixed decimal to a comparable runtime identity. D can be a token identifier, a chain/token pair, or a base/quote market key. The fixed-decimal format continues to own fractional decimal places and unit representation; denomination is never used to infer or normalize scale.

func AddDenominatedAs added in v1.2.0

func AddDenominatedAs[
	ResultV FixedDecimalFormat[ResultU], ResultU Unit,
	D comparable,
	AV FixedDecimalFormat[AU], AU Unit,
	BV FixedDecimalFormat[BU], BU Unit,
](
	a Denominated[D, AV, AU],
	b Denominated[D, BV, BU],
) (Denominated[D, ResultV, ResultU], error)

AddDenominatedAs checks runtime denomination, exactly rescales both values to the selected result format, and adds them.

Example
package main

import (
	"fmt"

	"github.com/JekaMas/sailfish"
)

func main() {
	type Asset struct {
		Chain uint32
		Token string
	}
	type Price2 = sailfish.PriceInUint64Units[sailfish.DecimalPlaces2]
	type Price5 = sailfish.PriceInUint64Units[sailfish.DecimalPlaces5]

	asset := Asset{Chain: 1, Token: "USDC"}
	price2, _ := sailfish.NewFixedDecimal[Price2]("1.20")
	price5, _ := sailfish.NewFixedDecimal[Price5]("0.00003")
	left := sailfish.NewDenominated(asset, price2)
	right := sailfish.NewDenominated(asset, price5)

	sum, err := sailfish.AddDenominatedAs[Price5](left, right)
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println(sum.Denomination().Token)
	fmt.Println(sum.Decimal().String())
}
Output:
USDC
1.20003

func NewDenominated added in v1.2.0

func NewDenominated[D comparable, V FixedDecimalFormat[U], U Unit](
	denomination D,
	value FixedDecimal[V, U],
) Denominated[D, V, U]

NewDenominated binds value to denomination without changing either value.

func RescaleDenominated added in v1.2.0

func RescaleDenominated[
	ToV FixedDecimalFormat[ToU], ToU Unit,
	D comparable,
	FromV FixedDecimalFormat[FromU], FromU Unit,
](value Denominated[D, FromV, FromU]) (Denominated[D, ToV, ToU], error)

RescaleDenominated exactly rescales a value while preserving its runtime denomination. It never derives fractional decimal places from denomination.

func SubDenominatedAs added in v1.2.0

func SubDenominatedAs[
	ResultV FixedDecimalFormat[ResultU], ResultU Unit,
	D comparable,
	AV FixedDecimalFormat[AU], AU Unit,
	BV FixedDecimalFormat[BU], BU Unit,
](
	a Denominated[D, AV, AU],
	b Denominated[D, BV, BU],
) (Denominated[D, ResultV, ResultU], error)

SubDenominatedAs checks runtime denomination, exactly rescales both values to the selected result format, and subtracts them.

func (Denominated[D, V, U]) Add added in v1.2.0

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

Add adds values only when their denominations match.

func (Denominated[D, V, U]) Compare added in v1.2.0

func (d Denominated[D, V, U]) Compare(other Denominated[D, V, U]) (int, error)

Compare compares values only when their denominations match.

func (Denominated[D, V, U]) Decimal added in v1.2.0

func (d Denominated[D, V, U]) Decimal() FixedDecimal[V, U]

Decimal returns the fixed decimal by value.

func (Denominated[D, V, U]) Denomination added in v1.2.0

func (d Denominated[D, V, U]) Denomination() D

Denomination returns the runtime identity associated with the decimal.

func (Denominated[D, V, U]) Sub added in v1.2.0

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

Sub subtracts values only when their denominations match.

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"
	ErrDenominationMismatch               Error = "sailfish: denomination mismatch"
	ErrNilSource                          Error = "sailfish: nil source"
	ErrNilDestination                     Error = "sailfish: nil destination"
	ErrNilWorkspace                       Error = "sailfish: nil workspace"
	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 (IntegerConversions)
package main

import (
	"fmt"
	"math/big"

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

type exampleAmountFormat = sailfish.AmountInUint256Units[sailfish.DecimalPlaces18]

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

	// Integer conversions use already-scaled units. They do not rescale.
	value, err := codec.FromU256(uint256.Int{1_250_000_000_000_000_000})
	if err != nil {
		fmt.Println(err)
		return
	}
	var destination big.Int
	if err = value.ToBigInt(&destination); err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println(value.String())
	fmt.Println(destination.String())
}
Output:
1.250000000000000000
1250000000000000000
Example (RationalConversions)
package main

import (
	"fmt"
	"math/big"

	"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.FromBigRat(big.NewRat(385_351, 3_125))
	if err != nil {
		fmt.Println(err)
		return
	}
	var rational big.Rat
	var workspace sailfish.BigRatWorkspace
	if err = price.ToBigRat(&rational, &workspace); err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println(price.String())
	fmt.Println(rational.String())
}
Output:
123.31232
385351/3125
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 AddAs added in v1.2.0

func AddAs[
	ResultV FixedDecimalFormat[ResultU], ResultU Unit,
	AV FixedDecimalFormat[AU], AU Unit,
	BV FixedDecimalFormat[BU], BU Unit,
](a FixedDecimal[AV, AU], b FixedDecimal[BV, BU]) (FixedDecimal[ResultV, ResultU], error)

AddAs exactly rescales both operands to the selected result format and adds them. Use the same-format Add method when no rescaling is required.

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 Rescale added in v1.2.0

func Rescale[
	ToV FixedDecimalFormat[ToU], ToU Unit,
	FromV FixedDecimalFormat[FromU], FromU Unit,
](value FixedDecimal[FromV, FromU]) (FixedDecimal[ToV, ToU], error)

Rescale converts value to an explicitly selected format without rounding. Downscaling rejects discarded nonzero units; upscaling rejects overflow.

func SubAs added in v1.2.0

func SubAs[
	ResultV FixedDecimalFormat[ResultU], ResultU Unit,
	AV FixedDecimalFormat[AU], AU Unit,
	BV FixedDecimalFormat[BU], BU Unit,
](a FixedDecimal[AV, AU], b FixedDecimal[BV, BU]) (FixedDecimal[ResultV, ResultU], error)

SubAs exactly rescales both operands to the selected result format and subtracts them. Use the same-format Sub method when no rescaling is required.

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]) ToBigInt added in v1.1.0

func (d FixedDecimal[V, U]) ToBigInt(dst *big.Int) error

ToBigInt writes the already-scaled integer units into caller-owned dst. Reusing a destination with sufficient capacity avoids allocation. A fresh destination may allocate its math/big backing words once.

func (FixedDecimal[V, U]) ToBigRat added in v1.2.0

func (d FixedDecimal[V, U]) ToBigRat(dst *big.Rat, workspace *BigRatWorkspace) error

ToBigRat writes the exact decimal value into caller-owned dst. Reusing dst and workspace avoids steady-state allocation. The first wide call may grow their math/big backing words once.

func (FixedDecimal[V, U]) ToU256 added in v1.1.0

func (d FixedDecimal[V, U]) ToU256() uint256.Int

ToU256 returns the already-scaled integer units as one inline four-limb value. Every supported backend fits exactly, so conversion cannot fail.

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]) FromBigInt added in v1.1.0

func (FixedDecimalCodec[V, U]) FromBigInt(source *big.Int) (FixedDecimal[V, U], error)

FromBigInt constructs a fixed decimal from non-negative, already-scaled integer units. It reads source without retaining or modifying it.

func (FixedDecimalCodec[V, U]) FromBigRat added in v1.2.0

func (c FixedDecimalCodec[V, U]) FromBigRat(source *big.Rat) (FixedDecimal[V, U], error)

FromBigRat constructs a fixed decimal only when source has an exact representation at the format's fractional decimal places. It performs no rounding and does not retain or modify source.

func (FixedDecimalCodec[V, U]) FromU256 added in v1.1.0

func (FixedDecimalCodec[V, U]) FromU256(source uint256.Int) (FixedDecimal[V, U], error)

FromU256 constructs a fixed decimal from non-negative, already-scaled uint256 units. Narrow backends reject values outside their integer width.

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