Documentation
¶
Overview ¶
The native backend: libhypercast linked into the binary. The core is a static library under staticlib/{goos}_{goarch}/, named on the cgo link line below, and every door is an ordinary C call to a symbol the linker resolved. Nothing is embedded, nothing is written to a temp directory, nothing is loaded at run time: a binary carries the core for its own platform, starts without touching the filesystem, runs from a read-only or `scratch` image, and has nothing that can fail to load.
That takes cgo, and so a C compiler wherever the module is built — gcc or clang on Linux, the Xcode command-line tools on macOS, a MinGW-w64 gcc (or llvm-mingw on arm64) on Windows. TinyGo compiling to WebAssembly links the same core from backend_tinygo.go — its cgo cannot parse this file's per-platform #cgo lines, hence `!tinygo` above. Anything else does not compile; unsupported.go says so by name. No other backend would reach a platform this one does not: a purego one extracting an embedded shared library, a cgo one loading that library instead of linking it, or a wasmtime one, since wasmtime-go itself needs cgo and ships engines only for these same platforms.
One archive serves both C libraries on Linux. cgo has no build constraint that tells glibc from musl, so there cannot be one per libc; the archive is the core built for the musl target, which asks the C library for nothing glibc and musl do not both have (memcpy, memset, bcmp). The suite runs against it on Debian and on Alpine.
Windows links the same MSVC archive C#'s Native AOT publish does: MinGW's linker reads MSVC's COFF objects, and what the archive asks of the C runtime — memcpy, memset, memcmp, and on x64 _fltused — resolves against msvcrt.dll through MinGW's own import library, so the link line names nothing else. The core imports nothing from Windows itself: parsing needs no system call.
iOS and Mac Catalyst link an archive of their own, since Go builds for both as GOOS=ios and every Mach-O object says which platform it was built for. GOOS=ios also satisfies the darwin constraint, so the macOS lines below say !ios. The three that share ios/arm64 are told apart by build tag: `maccatalyst`, which gomobile sets for that target; `iossimulator`, which nothing sets, so a simulator build passes it by hand; and neither, for a device. There is no archive for the simulator on amd64.
Android links an archive of its own too, for arm64 and amd64: the core built for the Android targets, which asks Bionic for what it asks musl for. GOOS=android satisfies the linux constraint, so the Linux lines below say !android. Building for it takes the NDK's clang as CC (CC=$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21-clang GOOS=android GOARCH=arm64 CGO_ENABLED=1), as any cgo package on Android does.
cgo cannot call a function pointer directly — it needs a statically-typed C call site — hence one shim per ABI shape (plain, numeric, unix, version), each taking its door as a pointer. The shims also own the out-params: any Go pointer handed to a cgo call escapes to the heap, so instead of passing `&out`, `&fault` and `&format` across, the format and the day periods arrive BY VALUE, the C side declares them, the out-value and the fault span on its own stack, and hands the whole verdict back BY VALUE as one struct. No Go pointer crosses but the input bytes — a pointer value into memory that already exists, not an address taken of a local — and the success path allocates nothing (allocs_test.go holds that).
The archives are committed (staticlib/README.md): a Go module is whatever is in the tree at the resolved version, with no packing step to stage them in. The hypercast_local build tag links the archive .github/scripts/local-core.sh builds from the checkout instead, so the suite can run against the core as it stands without replacing a committed archive.
Package hypercast provides allocation-lean scalar casts — booleans, numerics, UUIDs, temporals — by calling the Rust core, linked into the binary as a static library through cgo (backend_static.go). It builds on Linux, macOS and Windows, on amd64 and arm64, with a C compiler present, and under TinyGo for WebAssembly, browser included (backend_tinygo.go); anything else is a compile error that says so (unsupported.go).
Every door returns Go's own union idiom: (value, *Fault), where a nil fault is the success case and a non-nil one carries the closed reason plus the offending byte span. Never an error for bad data in the exception sense — *Fault implements error for composition, but the doors never panic on input; a panic here means a caller bug (a malformed NumFormat or an undefined enum value), never data.
Door names follow the native ABI's (cast_i32 is I32, cast_timestamp is Timestamp) so the polyglot surface reads the same across bindings, except where Go already owns the word: Exact and Span return the Decimal and Duration types their ABI names would collide with, and DateOnly, DateOnlyOrdered and TimeOfDay say what cast_date, cast_date_ordered and cast_time return. Doors are generic over string | []byte — both cross zero-copy (the core only reads).
Go-flavored fidelity, stated honestly both ways: time.Time carries full nanoseconds across the whole 0001–9999 window, and time-of-day comes back as a time.Duration since midnight, also nanosecond-exact. But time.Duration's int64-nanosecond ceiling (±292 years) sits far below the core's ±10,000-year duration window, so the duration door returns the protobuf pair (Duration{Seconds, Nanos}) with a checked AsDuration() converter rather than silently wrapping.
Index ¶
- Constants
- Variables
- func Available() bool
- func DateOnly[T Text](text T) (Date, *Fault)
- func DateOnlyOrdered[T Text](text T, order DateOrder) (Date, *Fault)
- func DateTime[T Text](text T, order DateOrder) (CivilDateTime, *Fault)
- func DateTimeWith[T Text](text T, format DateFormat) (CivilDateTime, *Fault)
- func Exact[T Text](text T, format NumFormat) (Decimal, *Fault)
- func ExactFromFloat64(value float64) (Decimal, *Fault)
- func ExcelDuration(value float64) (Duration, *Fault)
- func ExcelSerialFromFloat64(value float64, epoch ExcelEpoch) (CivilDateTime, *Fault)
- func FormatVersion(packed uint32) string
- func LoadError() error
- func NativeVersion() string
- func Span[T Text](text T) (Duration, *Fault)
- type CastFailure
- type CivilDateTime
- type Date
- type DateFormat
- type DateOrder
- type Decimal
- type Duration
- type ExcelEpoch
- type Fault
- func Bool[T Text](text T) (bool, *Fault)
- func Char[T Text](text T) (rune, *Fault)
- func ExcelSerial[T Text](text T, epoch ExcelEpoch) (time.Time, *Fault)
- func ExcelTime(value float64) (time.Duration, *Fault)
- func F32[T Text](text T, format NumFormat) (float32, *Fault)
- func F64[T Text](text T, format NumFormat) (float64, *Fault)
- func FaultFromCode(code, offset, length uint32) (*Fault, bool)
- func I8[T Text](text T, format NumFormat) (int8, *Fault)
- func I16[T Text](text T, format NumFormat) (int16, *Fault)
- func I32[T Text](text T, format NumFormat) (int32, *Fault)
- func I64[T Text](text T, format NumFormat) (int64, *Fault)
- func Numeric[V Number, T Text](text T, format NumFormat) (V, *Fault)
- func Scalar[V ScalarTarget, T Text](text T, format NumFormat) (V, *Fault)
- func TimeOfDay[T Text](text T) (time.Duration, *Fault)
- func Timestamp[T Text](text T) (time.Time, *Fault)
- func U8[T Text](text T, format NumFormat) (uint8, *Fault)
- func U16[T Text](text T, format NumFormat) (uint16, *Fault)
- func U32[T Text](text T, format NumFormat) (uint32, *Fault)
- func U64[T Text](text T, format NumFormat) (uint64, *Fault)
- func Unix[T Text](text T, precision UnixPrecision) (time.Time, *Fault)
- func Uuid[T Text](text T) (uuid.UUID, *Fault)
- type NumFormat
- type NumStyles
- type Number
- type RawCivil
- type RawDate
- type RawDayPeriods
- type RawNumFormat
- type RawSymbol8
- type RawSymbol16
- type RawTimestamp
- type ScalarTarget
- type Text
- type UnixPrecision
Examples ¶
Constants ¶
const CurrencyMaxBytes = 16
CurrencyMaxBytes is the inline capacity of the ABI's currency symbol, in UTF-8 bytes.
const DesignatorMaxBytes = 16
DesignatorMaxBytes is the inline capacity of the ABI's day-period designators, in UTF-8 bytes.
const RawDayPeriodsSize = uint32(unsafe.Sizeof(RawDayPeriods{}))
RawDayPeriodsSize is the size word a RawDayPeriods carries: its own 40 bytes.
const RawNumFormatSize = uint32(unsafe.Sizeof(RawNumFormat{}))
RawNumFormatSize is the size word a RawNumFormat carries: its own 92 bytes.
const SymbolMaxBytes = 8
SymbolMaxBytes is the inline capacity of the ABI's separators, signs and percent symbol, in UTF-8 bytes: enough for every separator a real culture declares (one or two characters).
Variables ¶
var Detect = NumFormat{DecimalSep: ".", GroupSep: ",", Styles: AllStyles | SeparatorDetect}
Detect is the detection profile — every lenience on, './,' roles resolved per input by SeparatorDetect's structural rules.
var Invariant = NumFormat{DecimalSep: ".", GroupSep: ",", Styles: AllStyles}
Invariant is the invariant profile — '.' decimal, ',' grouping, every lenience on, no currency symbol or other spelling declared.
Functions ¶
func Available ¶ added in v0.3.0
func Available() bool
Available reports whether the core can be called. The core is linked into the binary (backend_static.go), so there is nothing to load and nothing that can fail: it is always true. It stays for code written against the load probe every binding carries.
func DateOnlyOrdered ¶
DateOnlyOrdered casts a separated calendar date — three digit fields joined by one consistent separator (/, -, or .) — under the caller-declared DateOrder. The year field is four digits wherever the order puts it (two-digit years mean century guessing, which never happens here); an undefined order is a caller bug and panics, never a verdict. The strict DateOnly door keeps rejecting every separated form.
Example ¶
package main
import (
"fmt"
hypercast "github.com/SkunkWerkx/HyperCast/go"
)
func main() {
// The same text is January 7th or July 1st only because the caller said which.
us, _ := hypercast.DateOnlyOrdered("1/7/2026", hypercast.MonthDayYear)
gb, _ := hypercast.DateOnlyOrdered("1/7/2026", hypercast.DayMonthYear)
fmt.Println(us.Month, us.Day)
fmt.Println(gb.Month, gb.Day)
}
Output: January 7 July 1
func DateTime ¶
func DateTime[T Text](text T, order DateOrder) (CivilDateTime, *Fault)
DateTime casts a zone-less civil date-time — the shape untrusted feeds actually send ("1/7/2026 3:04 PM", "2026-01-07 15:04:05") — under the caller-declared DateOrder. The date part follows DateOnlyOrdered's grammar; the optional time part (one space or 'T' after the date) is 24-hour h:mm[:ss[.f]] or 12-hour with an AM/PM marker, after the clock or ahead of it; absent, the time is midnight. No zone is read and none is invented — Timestamp stays the strict RFC 3339 instant door. The markers are the English AM/PM only; DateTimeWith reads a culture's own beside them. An undefined order is a caller bug and panics, never a verdict.
Example ¶
package main
import (
"fmt"
"log"
hypercast "github.com/SkunkWerkx/HyperCast/go"
)
func main() {
// Zone-less text names a wall-clock date and time, not an instant — so that is what
// comes back.
civil, fault := hypercast.DateTime("1/7/2026 3:04 PM", hypercast.MonthDayYear)
if fault != nil {
log.Fatal(fault)
}
fmt.Println(civil.Date.Year, civil.Date.Month, civil.Date.Day, civil.TimeOfDay)
}
Output: 2026 January 7 15h4m0s
func DateTimeWith ¶ added in v0.9.0
func DateTimeWith[T Text](text T, format DateFormat) (CivilDateTime, *Fault)
DateTimeWith is DateTime under a declared DateFormat: the same grammar under its Order, with its AM and PM designators read beside the English markers ("1/7/2026 3:04 nachm.", "2026-01-07 오후 3:04"). A format declaring no designators reads exactly what DateTime reads. An undefined order or a designator the core would refuse is a caller bug and panics, never a verdict.
Example ¶
package main
import (
"fmt"
"log"
hypercast "github.com/SkunkWerkx/HyperCast/go"
)
func main() {
// The culture's own day periods, declared beside the order; the English AM/PM are read
// whatever is declared.
german := hypercast.DateFormat{Order: hypercast.DayMonthYear, AM: "vorm.", PM: "nachm."}
civil, fault := hypercast.DateTimeWith("1.7.2026 3:04 nachm.", german)
if fault != nil {
log.Fatal(fault)
}
korean := hypercast.DateFormat{Order: hypercast.YearMonthDay, AM: "오전", PM: "오후"}
seoul, _ := hypercast.DateTimeWith("2026-07-01 오후 3:04", korean)
fmt.Println(civil.Date.Day, civil.Date.Month, civil.TimeOfDay, civil == seoul)
}
Output: 1 July 15h4m0s true
func Exact ¶ added in v0.3.0
Exact casts decimal text to a Decimal under the declared format — the same grammar and NumFormat as the real doors (declared separators and grouping, accounting parentheses, exponent, trailing percent, currency symbol) — but never rounded: a magnitude past 2^96 - 1 or a scale past 28 is OutOfRange, not approximated. Named Exact because the result type already owns the identifier Decimal, the way Span returns a Duration.
Example ¶
package main
import (
"fmt"
"log"
hypercast "github.com/SkunkWerkx/HyperCast/go"
)
func main() {
dollars := hypercast.NumFormat{DecimalSep: ".", GroupSep: ",", Styles: hypercast.AllStyles, Currency: "$"}
d, fault := hypercast.Exact("($1,234.50)", dollars)
if fault != nil {
log.Fatal(fault)
}
// Never rounded, and canonical: the trailing zero is trimmed.
fmt.Println(d, d.Scale, d.Negative)
}
Output: -1234.5 1 true
func ExactFromFloat64 ¶ added in v0.7.0
ExactFromFloat64 reads a number the caller already holds — the float64 a workbook stores for a numeric cell — as the exact Decimal it names: the shortest decimal that rounds back to the float64, the digits a spreadsheet writes for it. So 0.1 is one tenth, not the binary fraction nearest it, and 0.1 + 0.2 is 0.30000000000000004 — the same digits strconv.FormatFloat(v, 'g', -1, 64) prints. The result is canonical, as Exact's is. NaN is Malformed; an infinity, a magnitude past 2^96 - 1 or more than 28 places is OutOfRange — the digits are never cut to fit. A typed door's Fault has no span: Offset and Length are 0.
Example ¶
package main
import (
"fmt"
"log"
hypercast "github.com/SkunkWerkx/HyperCast/go"
)
func main() {
// The digits a spreadsheet writes for the double, not the binary fraction nearest it.
// (Two variables, because Go adds the constants 0.1 + 0.2 exactly, to 0.3.)
a, b := 0.1, 0.2
d, fault := hypercast.ExactFromFloat64(a + b)
if fault != nil {
log.Fatal(fault)
}
fmt.Println(d)
}
Output: 0.30000000000000004
func ExcelDuration ¶ added in v0.7.0
ExcelDuration reads a number of days the caller already holds as a float64 — what an elapsed-time format ([h]:mm:ss) stores — as the protobuf pair Span returns: 1.5 is a day and twelve hours, and a negative span is negative, its size snapped as a serial's time. NaN or an infinity is Malformed; beyond ±10,000 years is OutOfRange.
func ExcelSerialFromFloat64 ¶ added in v0.7.0
func ExcelSerialFromFloat64(value float64, epoch ExcelEpoch) (CivilDateTime, *Fault)
ExcelSerialFromFloat64 reads an Excel date serial the caller already holds as a float64 under a caller-declared ExcelEpoch — the twin of ExcelSerial for a workbook reader that has the cell's number and no text. The result is the zone-less wall clock the cell holds, a CivilDateTime as DateTime returns, its fraction snapped: the time with the fewest fractional-second digits that the same float64 stores, so Excel's 23:59:59 is read on the second rather than the nanoseconds of float noise the double carries. The 1900 system's phantom serial 60, a serial below the system's first day and one past 9999-12-31 are OutOfRange; a negative, NaN or infinite serial is Malformed. An undefined epoch is a caller bug and panics, never a verdict.
Example ¶
package main
import (
"fmt"
"log"
hypercast "github.com/SkunkWerkx/HyperCast/go"
)
func main() {
civil, fault := hypercast.ExcelSerialFromFloat64(45292.75, hypercast.Excel1900)
if fault != nil {
log.Fatal(fault)
}
fmt.Println(civil.Date.Year, civil.Date.Month, civil.Date.Day, civil.TimeOfDay)
}
Output: 2024 January 1 18h0m0s
func FormatVersion ¶ added in v0.7.0
FormatVersion renders a native library's packed version word — major<<16 | minor<<8 | patch, as a *_version() export returns it — as "major.minor.patch".
func LoadError ¶ added in v0.4.0
func LoadError() error
LoadError returns why the core could not be loaded. A linked core always can, so it is always nil. Kept for the reason Available is.
func NativeVersion ¶ added in v0.3.0
func NativeVersion() string
NativeVersion reports the linked core's own version as "major.minor.patch" — read from the core itself, not from this module — so a deployment can confirm which build of the archive went into the binary. It cannot fail.
Example ¶
package main
import (
"log"
hypercast "github.com/SkunkWerkx/HyperCast/go"
)
func main() {
// The version the linked core reports about itself, "major.minor.patch".
log.Printf("hypercast core %s", hypercast.NativeVersion())
}
Output:
func Span ¶
Span casts a duration (ISO 8601 fixed components, invariant colon form, or protobuf JSON seconds) to the protobuf pair — see Duration for why not time.Duration directly.
Example ¶
package main
import (
"fmt"
"log"
hypercast "github.com/SkunkWerkx/HyperCast/go"
)
func main() {
span, fault := hypercast.Span("PT1H30M15.5S")
if fault != nil {
log.Fatal(fault)
}
fmt.Println(span.Seconds, span.Nanos)
// The checked converter: time.Duration tops out near 292 years, the core's window
// does not.
std, ok := span.AsDuration()
fmt.Println(std, ok)
}
Output: 5415 500000000 1h30m15.5s true
Types ¶
type CastFailure ¶
type CastFailure int32
CastFailure is the closed set of reasons a cast can fail — the native core's verdict codes, verbatim.
const ( // Empty means required input was empty or whitespace. Empty CastFailure = 1 // Malformed means input was present but not recognizable as the target type. Malformed CastFailure = 2 // OutOfRange means well-formed but outside the target's range — "256" for a U8, // 1e400 for an F64. OutOfRange CastFailure = 3 )
func ReasonFromCode ¶ added in v0.7.0
func ReasonFromCode(code uint32) (CastFailure, bool)
ReasonFromCode is the reason whose verdict code is code (1 empty, 2 malformed, 3 out of range), and false for any other value — 0, the code for success, included.
func (CastFailure) String ¶
func (f CastFailure) String() string
String names the reason in lower case — "empty", "malformed", "out of range" — the form Fault.Error embeds.
type CivilDateTime ¶
CivilDateTime is a wall-clock date and time with no zone — exactly what zone-less text like "1/7/2026 3:04 PM" actually names. Deliberately not a time.Time: without a zone there is no instant, and inventing one (assuming UTC, say) would be a silent value error of up to ±14 hours. Fuse a zone yourself when you know it: time.Date(d.Date.Year, d.Date.Month, d.Date.Day, 0, 0, 0, 0, loc).Add(d.TimeOfDay).
type Date ¶
Date is a calendar date with no time or zone — the protobuf google.type.Date fields. Go has no standard date-only type; this keeps the core's digits exactly.
type DateFormat ¶ added in v0.9.0
type DateFormat struct {
Order DateOrder
// AM is the designator of the first half of the day; empty declares none beside AM.
AM string
// PM is the designator of the second half; empty declares none beside PM.
PM string
}
DateFormat is the caller-declared notation of a zone-less civil date-time for DateTimeWith: the field order, and the words the culture writes for the two halves of a 12-hour clock ("vorm."/"nachm.", "오전"/"오후", "a. m."/"p. m."), the way NumFormat declares a numeric notation. Declared, never looked up — Go's standard library carries no culture data, so an application fills these from its own. The English AM/PM are read whatever is declared, ASCII case-insensitively; a declared designator is matched byte for byte, after the clock ("3:04 nachm.", optional space before) or ahead of it ("오후 3:04", optional space after). Empty designators declare nothing, which is exactly what DateTime reads.
A designator the core would refuse is a caller bug and panics, never a verdict: longer than DesignatorMaxBytes of UTF-8, not valid UTF-8, carrying an ASCII digit, or starting or ending in ASCII whitespace (the grammar around it reads that; an inner space, as in "a. m.", is fine). DateFormat.Raw reports the same check as an error.
func (DateFormat) Raw ¶ added in v0.9.0
func (f DateFormat) Raw() (raw RawDayPeriods, err error)
Raw validates the format's designators and lays them out as the core reads them — what DateTimeWith does with its format argument, as an error rather than the door's panic. The core's rules verbatim: each designator empty or at most DesignatorMaxBytes of valid UTF-8 with no ASCII digit and no ASCII whitespace at either end. The order is not checked here: it crosses beside the designators, and DateOrder.Valid reports it.
type DateOrder ¶
type DateOrder uint32
DateOrder is the caller-declared field order of a separated calendar date — no guessing, ever: "1/7/2026" is January 7th (MonthDayYear, the en-US order) or July 1st (DayMonthYear, the en-GB order) only because the caller said which.
The declared field orders — the native core's codes, verbatim.
func DateOrderFromCode ¶ added in v0.7.0
DateOrderFromCode is the order whose ABI discriminant is code (1 year-month-day, 2 month-day-year, 3 day-month-year), and false for any other value.
type Decimal ¶ added in v0.3.0
type Decimal struct {
// Lo is the low 64 bits of the magnitude.
Lo uint64
// Hi is the high 32 bits of the magnitude; the magnitude never exceeds 2^96 - 1.
Hi uint32
// Scale is the number of places the magnitude is shifted right, 0..=28.
Scale uint8
// Negative is true for a negative value; zero is never negative.
Negative bool
}
Decimal is an exact decimal — a sign, a 96-bit unsigned magnitude, and a base-10 scale — the shape .NET's decimal stores natively and every arbitrary-precision decimal builds from directly. The value is (-1)^Negative × (Hi·2^64 + Lo) × 10^-Scale, Scale 0..=28. Never rounded: the Exact door reports text carrying more precision than that as OutOfRange rather than silently approximating. Canonical: exact trailing zeros in the fraction are trimmed so the scale is minimal ("1.10", "1.1" and "1.1000" are all magnitude 11, scale 1; zero is scale 0), and zero is never negative. Comparable, so two Decimals are == exactly when their values agree.
type Duration ¶
Duration is the protobuf pair — whole seconds plus same-signed nanoseconds. Returned instead of time.Duration because the core's ±10,000-year window exceeds time.Duration's int64-nanosecond ceiling (±292 years); AsDuration converts when the value fits.
type ExcelEpoch ¶
type ExcelEpoch uint32
ExcelEpoch is the date system an Excel serial number is expressed in. Spreadsheets carry no marker for this — it is a workbook-level setting — so the caller states it, the same way UnixPrecision and DateOrder are declared rather than guessed.
const ( // Excel1900 is the Windows default: serial 1 is 1900-01-01, and serial 60 is a // February 29th that never existed. Excel1900 ExcelEpoch = 1 // Excel1904 is the legacy Macintosh system, still selectable today: serial 0 is // 1904-01-01, with no phantom day anywhere in it. Excel1904 ExcelEpoch = 2 )
The declared Excel date systems — the native core's codes, verbatim.
func ExcelEpochFromCode ¶ added in v0.7.0
func ExcelEpochFromCode(code uint32) (ExcelEpoch, bool)
ExcelEpochFromCode is the date system whose ABI discriminant is code (1 the 1900 system, 2 the 1904 system), and false for any other value.
func (ExcelEpoch) Valid ¶ added in v0.7.0
func (e ExcelEpoch) Valid() bool
Valid reports whether e is a defined date system, the ones a door accepts.
type Fault ¶
type Fault struct {
Reason CastFailure
Offset int
Length int
}
Fault is the failure case of a cast: a closed reason plus the offending span, as byte offsets into the input (identical to what the door received — string and []byte inputs are both raw UTF-8 here). Implements error so verdicts compose with Go's error plumbing, but nothing is captured or formatted until Error() is actually called.
func Bool ¶
Bool casts boolean text: true/false plus the conventions untrusted sources actually send (t/f, yes/no, y/n, 1/0, on/off, enabled/disabled, active/inactive, checked/unchecked, in/out), ASCII case-insensitive.
Example ¶
package main
import (
"fmt"
hypercast "github.com/SkunkWerkx/HyperCast/go"
)
func main() {
yes, _ := hypercast.Bool("yes")
off, _ := hypercast.Bool([]byte("OFF")) // string and []byte both cross zero-copy
_, fault := hypercast.Bool("maybe")
fmt.Println(yes, off, fault.Reason)
}
Output: true false malformed
func Char ¶ added in v0.8.0
Char casts one character: text that is exactly one UTF-8 scalar is that scalar verbatim, before any trimming (" " is a space, "6" is the digit six); otherwise, ASCII whitespace trimmed, exactly one declared code point — decimal 65, U+0041, 0x41, &H41, or an HTML numeric entity A / A (the ; required), prefixes ASCII case-insensitive. A code point past U+10FFFF or a surrogate is OutOfRange; the rune returned is always a valid Unicode scalar.
Example ¶
package main
import (
"fmt"
hypercast "github.com/SkunkWerkx/HyperCast/go"
)
func main() {
// One character, verbatim or as a declared code point.
a, _ := hypercast.Char("A")
e, _ := hypercast.Char("U+00E9")
smile, _ := hypercast.Char("😀")
_, fault := hypercast.Char("U+D800")
fmt.Println(string(a), string(e), string(smile), fault.Reason)
}
Output: A é 😀 out of range
func ExcelSerial ¶
func ExcelSerial[T Text](text T, epoch ExcelEpoch) (time.Time, *Fault)
ExcelSerial casts an Excel date serial under a caller-declared ExcelEpoch to a UTC time.Time. The whole part counts days from the system's own day zero and the fraction is the time of day, so "45292.75" is 2024-01-01T18:00:00Z. A spreadsheet cell carries no zone and none is invented.
The 1900 system contains a day that never existed: serial 60 is 1900-02-29, kept deliberately because Lotus 1-2-3 wrongly treated 1900 as a leap year and Excel copied the bug for file compatibility. It is OutOfRange here — the same verdict DateOnly gives the text "1900-02-29" — so every serial above it is shifted one day against a naive count, which is the arithmetic hand-rolled conversions get wrong.
An undefined epoch is a caller bug and panics, never a verdict.
Example ¶
package main
import (
"fmt"
"log"
hypercast "github.com/SkunkWerkx/HyperCast/go"
)
func main() {
ts, fault := hypercast.ExcelSerial("45292.75", hypercast.Excel1900)
if fault != nil {
log.Fatal(fault)
}
fmt.Println(ts)
}
Output: 2024-01-01 18:00:00 +0000 UTC
func ExcelTime ¶ added in v0.7.0
ExcelTime reads the fraction of an Excel serial the caller already holds as a float64 as a time.Duration since midnight, as TimeOfDay returns: 0.75 and 45292.75 are both 18h. Snapped as ExcelSerialFromFloat64 snaps, and a fraction that snaps to a whole day is midnight. A negative, NaN or infinite serial is Malformed; one past 9999-12-31 is OutOfRange.
func F32 ¶
F32 casts real text under the declared format: finite values only (NaN/Infinity literals are Malformed, overflow to infinity is OutOfRange), declared separators and grouping, accounting parentheses, exponent, and trailing percent (50% is 0.5).
func F64 ¶
F64 casts real text to float64. Rules as F32.
Example ¶
package main
import (
"fmt"
"log"
hypercast "github.com/SkunkWerkx/HyperCast/go"
)
func main() {
// The caller declares the notation; nothing is guessed from the input.
euros := hypercast.NumFormat{DecimalSep: ",", GroupSep: ".", Styles: hypercast.AllStyles, Currency: "€"}
value, fault := hypercast.F64("€ 1.234,50", euros)
if fault != nil {
log.Fatal(fault)
}
fmt.Println(value)
}
Output: 1234.5
Example (Detect) ¶
package main
import (
"fmt"
hypercast "github.com/SkunkWerkx/HyperCast/go"
)
func main() {
// Detect resolves the separators' roles from the input's own structure — and refuses
// to guess when the structure does not settle it.
value, _ := hypercast.F64("1.234.567,89", hypercast.Detect)
_, fault := hypercast.F64("1,000", hypercast.Detect)
fmt.Println(value, fault.Reason)
}
Output: 1.23456789e+06 malformed
func FaultFromCode ¶ added in v0.7.0
FaultFromCode is the fault a nonzero verdict code and its byte span name, and false when the code names no reason — a binding bug, not data.
func I8 ¶
I8 casts integer text under the declared format: the type's own range, declared grouping, accounting parentheses, non-negative exponent (1e3 is 1000; a decimal point is never accepted), and 0x/&H/0b two's-complement radix prefixes (0xFF is -1). The other integer doors share these rules at their own widths.
func I32 ¶
I32 casts integer text to int32. Rules as I8.
Example ¶
package main
import (
"fmt"
"log"
hypercast "github.com/SkunkWerkx/HyperCast/go"
)
func main() {
// Accounting parentheses and grouping, under the declared invariant format.
value, fault := hypercast.I32("(1,234)", hypercast.Invariant)
if fault != nil {
log.Fatal(fault)
}
fmt.Println(value)
}
Output: -1234
Example (Fault) ¶
package main
import (
"fmt"
hypercast "github.com/SkunkWerkx/HyperCast/go"
)
func main() {
// A verdict, not an exception: the closed reason and the byte span that offended.
_, fault := hypercast.I32(" 12x4", hypercast.Invariant)
fmt.Println(fault.Reason, fault.Offset, fault.Length)
fmt.Println(fault)
_, fault = hypercast.U8("256", hypercast.Invariant)
fmt.Println(fault.Reason == hypercast.OutOfRange)
}
Output: malformed 4 1 hypercast: malformed at byte 4..5 true
func Numeric ¶ added in v0.3.0
Numeric casts numeric text to V under the declared format by dispatching to V's own door — Numeric[int32] is I32, Numeric[Decimal] is Exact — with T inferred from the argument: hypercast.Numeric[int32]("42", format). The rules and verdicts are the concrete door's, unchanged. An unsupported V is impossible by construction: Number lists exactly the types that have a door, so the switch below is exhaustive and the compiler rejects anything else at the call site.
Example ¶
package main
import (
"fmt"
hypercast "github.com/SkunkWerkx/HyperCast/go"
)
func main() {
// One door generic over the target: Numeric[int32] is I32, Numeric[Decimal] is Exact.
n, _ := hypercast.Numeric[int32]("42", hypercast.Invariant)
f, _ := hypercast.Numeric[float64]("50%", hypercast.Invariant)
d, _ := hypercast.Numeric[hypercast.Decimal]("1.10", hypercast.Invariant)
fmt.Println(n, f, d)
}
Output: 42 0.5 1.1
func Scalar ¶ added in v0.8.0
func Scalar[V ScalarTarget, T Text](text T, format NumFormat) (V, *Fault)
Scalar casts text to V by dispatching to V's own door, the way Numeric does for the numeric targets: Scalar[bool] is Bool, Scalar[uuid.UUID] is Uuid, Scalar[time.Time] is Timestamp (RFC 3339, zone mandatory, normalized to UTC), Scalar[Date] is DateOnly (strict yyyy-MM-dd), Scalar[Duration] is Span, and every Number is Numeric. format is read only by the numeric doors. The rules and verdicts are the concrete door's, unchanged, and an unsupported V is a compile error: ScalarTarget lists exactly the types that have a door.
Example ¶
package main
import (
"fmt"
"time"
hypercast "github.com/SkunkWerkx/HyperCast/go"
)
func main() {
// One door generic over every target with a door of its own: Scalar[bool] is Bool,
// Scalar[time.Time] is Timestamp, Scalar[int32] is I32.
b, _ := hypercast.Scalar[bool]("yes", hypercast.Invariant)
at, _ := hypercast.Scalar[time.Time]("2026-01-02T15:04:05+05:00", hypercast.Invariant)
n, _ := hypercast.Scalar[int32]("1,234", hypercast.Invariant)
fmt.Println(b, at.Format(time.RFC3339), n)
}
Output: true 2026-01-02T10:04:05Z 1234
func TimeOfDay ¶
TimeOfDay casts an ISO 24-hour time-of-day to a time.Duration since midnight — nanosecond-exact, and comfortably inside time.Duration's range.
Example ¶
package main
import (
"fmt"
"log"
hypercast "github.com/SkunkWerkx/HyperCast/go"
)
func main() {
sinceMidnight, fault := hypercast.TimeOfDay("15:04:05.5")
if fault != nil {
log.Fatal(fault)
}
fmt.Println(sinceMidnight)
}
Output: 15h4m5.5s
func Timestamp ¶
Timestamp casts an RFC 3339 instant — zone mandatory — to a UTC time.Time at full nanosecond fidelity across the whole 0001–9999 window.
Example ¶
package main
import (
"fmt"
"log"
hypercast "github.com/SkunkWerkx/HyperCast/go"
)
func main() {
// RFC 3339, zone mandatory, normalized to UTC at full nanosecond fidelity.
ts, fault := hypercast.Timestamp("2026-01-02T15:04:05.123456789+05:00")
if fault != nil {
log.Fatal(fault)
}
fmt.Println(ts)
}
Output: 2026-01-02 10:04:05.123456789 +0000 UTC
func Unix ¶
func Unix[T Text](text T, precision UnixPrecision) (time.Time, *Fault)
Unix casts an integer Unix-epoch value under a caller-declared unit to a UTC time.Time. An undefined precision is a caller bug and panics, never a verdict.
Example ¶
package main
import (
"fmt"
hypercast "github.com/SkunkWerkx/HyperCast/go"
)
func main() {
// The unit is declared, never inferred from the magnitude.
fromSeconds, _ := hypercast.Unix("1700000000", hypercast.Seconds)
fromMillis, _ := hypercast.Unix("1700000000000", hypercast.Milliseconds)
fmt.Println(fromSeconds, fromSeconds.Equal(fromMillis))
}
Output: 2023-11-14 22:13:20 +0000 UTC true
func Uuid ¶
Uuid casts UUID text — all five .NET Guid formats (D/N/B/P/X) plus urn:uuid:/GUID:/UUID: prefixes — to a uuid.UUID (RFC 9562 byte order, which is exactly uuid.UUID's own layout).
Example ¶
package main
import (
"fmt"
"log"
hypercast "github.com/SkunkWerkx/HyperCast/go"
)
func main() {
id, fault := hypercast.Uuid("urn:uuid:01020304-0506-0708-090a-0b0c0d0e0f10")
if fault != nil {
log.Fatal(fault)
}
fmt.Println(id)
}
Output: 01020304-0506-0708-090a-0b0c0d0e0f10
func (*Fault) Error ¶
Error renders the verdict as "hypercast: {reason} at byte {start}..{end}", the span as a half-open byte range into the input. A nil *Fault — a door's success, printed — renders as "<nil>", what fmt prints for one under stock Go after recovering the nil dereference. TinyGo's wasm targets cannot recover a panic, so without this the same print trapped there.
type NumFormat ¶
type NumFormat struct {
// DecimalSep is the decimal separator ('.' invariant, ',' in eurozone text): 1 to
// SymbolMaxBytes bytes of UTF-8 with no ASCII digit. Required.
DecimalSep string
// GroupSep is the digit-group separator (',' invariant, '.', ' ' or ", " elsewhere),
// likewise; must differ from DecimalSep. Required.
GroupSep string
Styles NumStyles
// Currency is the symbol the CurrencySymbol style accepts ("$", "€", "kr.", "CHF") —
// matched whole at the edges of the numeric body, never inside the digit scan, so a
// symbol containing a separator character is fine. Empty declares none.
Currency string
// CurrencyDecimalSep is the decimal separator of a currency amount, read in place of
// DecimalSep when CurrencySymbol is set, Currency is declared and the symbol is found
// in the text — the way a culture's currency pattern may group and point differently
// from its number pattern. Empty keeps DecimalSep. Ignored under SeparatorDetect, whose
// detection governs the whole token.
CurrencyDecimalSep string
// CurrencyGroupSep is the group separator of a currency amount, likewise; empty keeps
// GroupSep.
CurrencyGroupSep string
// MinusSign is a further spelling of the minus sign ("−", U+2212, in Estonian and
// Basque text), read wherever ASCII '-' is: ahead of the digits, before or after a
// leading currency symbol, and after the digits under TrailingSign. ASCII '-' is always
// read; empty declares no other.
MinusSign string
// PlusSign is a further spelling of the plus sign, likewise beside ASCII '+'.
PlusSign string
// Percent is a further spelling of the percent symbol ("٪" in Arabic-script text, "%"
// fullwidth), read as a trailing '%' is while Percent is set — the real and decimal
// doors; the integer doors never read a percent. ASCII '%' is always read.
Percent string
// DigitZero declares native digits beside ASCII 0–9: the ten code points from this zero
// up ('٠', U+0660, declares the Arabic-Indic digits; '0', U+FF10, the fullwidth ones),
// read wherever a digit is — integer part, fraction, exponent; a radix-prefixed (0x/0b)
// body stays ASCII. 0 declares none. An ASCII zero is a caller bug, as is a code point
// that starts no Unicode digit block.
DigitZero rune
}
NumFormat is the caller-declared numeric notation for the integer, real and decimal doors. The core carries no culture data, and neither does Go's standard library, so there is nothing here to bridge from — a call site parsing culture-sensitive text declares its format out loud (Invariant, or a literal filled from whatever culture data the application holds); there is no default, the same stance every binding in this repo takes. The two number separators are the only required fields; every other field declares a further spelling the core reads beside the ASCII one, so a format that declares nothing else reads exactly what Invariant reads, and nothing that parsed before a symbol was declared stops parsing once it is.
A format the core would refuse is a caller bug and panics, never a verdict: a number separator that is empty or longer than SymbolMaxBytes, equal number separators (or equal separators in force for a currency amount), any symbol that is not valid UTF-8 or carries an ASCII digit, a sign or percent symbol carrying ASCII whitespace, a Currency longer than CurrencyMaxBytes or carrying ASCII whitespace, or a DigitZero that starts no Unicode digit block. NumFormat.Raw reports the same check as an error.
Example ¶
package main
import (
"fmt"
hypercast "github.com/SkunkWerkx/HyperCast/go"
)
func main() {
// Everything past the separators declares a further spelling the core reads beside the
// ASCII one — the minus sign U+2212, a trailing sign, native digits — so "-5" keeps
// parsing under a format that also reads "−5", "5−" and "٥".
estonian := hypercast.NumFormat{
DecimalSep: ",", GroupSep: " ", Styles: hypercast.AllStyles | hypercast.TrailingSign,
Currency: "€", MinusSign: "−",
}
a, _ := hypercast.F64("−1 234,5 €", estonian)
b, _ := hypercast.F64("1 234,5 €−", estonian)
c, _ := hypercast.F64("-1234.5", hypercast.Invariant)
arabic := hypercast.NumFormat{DecimalSep: "٫", GroupSep: "٬", Styles: hypercast.AllStyles, DigitZero: '٠', Percent: "٪"}
d, _ := hypercast.F64("١٢٫٥٪", arabic)
fmt.Println(a, b, c, d)
}
Output: -1234.5 -1234.5 -1234.5 0.125
func (NumFormat) Raw ¶ added in v0.7.0
func (f NumFormat) Raw() (raw RawNumFormat, err error)
Raw validates the format and lays it out as the core reads it — what every numeric door does with its format argument, as an error rather than the doors' panic. The core's rules verbatim: each number separator one to SymbolMaxBytes UTF-8 bytes with no ASCII digit, the two different; each currency separator the same or empty, and the pair in force for a currency amount different; each sign and the percent symbol the same or empty, with no ASCII whitespace; the currency symbol at most CurrencyMaxBytes, no digit, no whitespace; the digit zero 0, or a code point outside ASCII that starts a run of ten digits sharing its UTF-8 prefix (every Unicode digit block does; one whose value modulo 64 exceeds 54 does not).
type NumStyles ¶
type NumStyles uint32
NumStyles are the lenience flags of NumFormat — bit-for-bit the native core's flags.
const ( // Grouping permits the group separator between digits (sizes not validated — between // digits is the rule). Grouping NumStyles = 1 << iota // Parentheses permits accounting negation: (1,234) is -1234. Parentheses // Exponent permits exponent notation. Integer doors reject a negative exponent. Exponent // RadixPrefixes permits 0x/&H/0b two's-complement prefixes (0xFF is -1 for an I8). RadixPrefixes // Percent permits a trailing %, dividing by 100. Real doors only. Percent // SeparatorDetect resolves the './,' roles per input from structure instead of the // declared separators (which are ignored while this flag is set). Detection, not // sniffing: a repeated separator is grouping (1.234.567,89); with both present the // rightmost is the decimal; a single separator with a non-3-digit right run is the // decimal (3,1415); with exactly 3 digits right, only a 0 integer part proves decimal // (0,785). Genuinely ambiguous input (12.185, 1,000) is Malformed at the separator, // never guessed. SeparatorDetect NumStyles = 1 << 5 // CurrencySymbol permits the format's declared Currency once, at either edge of the // numeric body: leading, before or after a sign ("$5", "-$5", "$ -5"), or trailing // ("5 €", "1.234,50 kr."), with optional ASCII whitespace between symbol and digits; // accounting parentheses wrap symbol and digits together ("($5)"). With no Currency // declared the flag matches nothing and changes nothing. A Currency declared while // this flag is off is Malformed at the symbol. Every integer, real and decimal door. CurrencySymbol NumStyles = 1 << 6 // TrailingSign permits the sign after the digits ("5-", "5 -", "5+", "5 €-", "5- €", // "2.5%-"), the way a culture whose negative pattern is "n-" writes it: ASCII '-'/'+' // or the declared MinusSign/PlusSign, once, with optional ASCII whitespace before it, // and never beside a leading sign or inside accounting parentheses. Not part of // AllStyles: a caller sets it from the culture's own negative pattern. TrailingSign NumStyles = 1 << 7 // AllStyles is every lenience on but TrailingSign, which is declared from a culture's // negative pattern rather than assumed. AllStyles = Grouping | Parentheses | Exponent | RadixPrefixes | Percent | CurrencySymbol )
type Number ¶ added in v0.3.0
type Number interface {
int8 | int16 | int32 | int64 | uint8 | uint16 | uint32 | uint64 | float32 | float64 | Decimal
}
Number is the closed set of targets the numeric doors cast to — exactly the eleven types I8 through U64, F32, F64 and Exact return. It exists for Numeric: a caller that is itself generic over the target names the type once instead of writing the door switch.
type RawCivil ¶ added in v0.7.0
type RawCivil struct {
Year uint16
Month uint8
Day uint8
Nanos uint64
// contains filtered or unexported fields
}
RawCivil is a zone-less wall clock as the core writes it: a date and the nanoseconds since its midnight. 16 bytes.
func (RawCivil) CivilDateTime ¶ added in v0.7.0
func (r RawCivil) CivilDateTime() CivilDateTime
CivilDateTime is the wall clock: the DateTime doors' value.
type RawDayPeriods ¶ added in v0.9.0
type RawDayPeriods struct {
Size uint32
AM RawSymbol16
PM RawSymbol16
// contains filtered or unexported fields
}
RawDayPeriods is a DateFormat's designators as they cross the ABI beside the declared DateOrder — 40 bytes, 4-aligned: a size word (RawDayPeriodsSize), the AM designator inline at byte 4 and the PM one at 21, each its bytes followed by its length, and two bytes of padding. A zero length declares nothing beside the English marker; where the core takes a pointer to one, null declares nothing at all. DateFormat.Raw builds one.
type RawNumFormat ¶ added in v0.7.0
type RawNumFormat struct {
Size uint32
Flags uint32
DigitZero uint32
DecimalSep RawSymbol8
GroupSep RawSymbol8
CurrencyDecimalSep RawSymbol8
CurrencyGroupSep RawSymbol8
MinusSign RawSymbol8
PlusSign RawSymbol8
Percent RawSymbol8
Currency RawSymbol16
}
RawNumFormat is NumFormat as it crosses the ABI — 92 bytes, 4-aligned, the core reading the caller's struct in place: a size word (RawNumFormatSize, so the layout can grow without another coordinated change), the flags, the native digits' zero code point (0 for none), then every symbol inline, its bytes followed by its length. Byte offsets: Flags 4, DigitZero 8, DecimalSep 12, GroupSep 21, CurrencyDecimalSep 30, CurrencyGroupSep 39, MinusSign 48, PlusSign 57, Percent 66, Currency 75. NumFormat.Raw builds one.
type RawSymbol8 ¶ added in v0.9.0
type RawSymbol8 struct {
Bytes [SymbolMaxBytes]byte
Len uint8
}
RawSymbol8 is a separator, sign or percent symbol as it crosses the ABI: its UTF-8 bytes held inline (zero-padded) and their count, 9 bytes with no alignment. A zero length declares none.
func (RawSymbol8) String ¶ added in v0.9.0
func (s RawSymbol8) String() string
String is the symbol's text — empty when none is declared.
type RawSymbol16 ¶ added in v0.9.0
type RawSymbol16 struct {
Bytes [CurrencyMaxBytes]byte
Len uint8
}
RawSymbol16 is a currency symbol or a day-period designator as it crosses the ABI, the same shape at 16 bytes of capacity: 17 bytes with no alignment.
func (RawSymbol16) String ¶ added in v0.9.0
func (s RawSymbol16) String() string
String is the symbol's text — empty when none is declared.
type RawTimestamp ¶ added in v0.7.0
RawTimestamp is an instant as the core writes it — protobuf's Timestamp: whole seconds from the Unix epoch and the nanoseconds after them. 16 bytes.
func (RawTimestamp) Time ¶ added in v0.7.0
func (r RawTimestamp) Time() time.Time
Time is the instant at UTC, at full nanosecond fidelity: the Timestamp, Unix and ExcelSerial doors' value.
type ScalarTarget ¶ added in v0.8.0
ScalarTarget is the closed set of types Scalar casts to: every Number, plus bool, uuid.UUID, time.Time, Date and Duration — each the one type a single door returns without a declared argument. Three are left out on purpose:
- rune: it is int32, so Scalar[int32] is I32's integer door; call Char for a character.
- time.Duration: TimeOfDay returns one, but a Go reader takes time.Duration for a span, and Span returns Duration because time.Duration cannot hold the core's window.
- CivilDateTime: zone-less date-times need a declared DateOrder; call DateTime.
type UnixPrecision ¶
type UnixPrecision uint32
UnixPrecision is the declared unit of a Unix-epoch value — no magnitude guessing, ever.
const ( Seconds UnixPrecision = 1 Milliseconds UnixPrecision = 2 Microseconds UnixPrecision = 3 Nanoseconds UnixPrecision = 4 )
The declared units a Unix-epoch value can carry — the native core's codes, verbatim.
func UnixPrecisionFromCode ¶ added in v0.7.0
func UnixPrecisionFromCode(code uint32) (UnixPrecision, bool)
UnixPrecisionFromCode is the precision whose ABI discriminant is code (1 seconds … 4 nanoseconds), and false for any other value.
func (UnixPrecision) Valid ¶ added in v0.7.0
func (p UnixPrecision) Valid() bool
Valid reports whether p is a defined precision, the ones a door accepts.
Directories
¶
| Path | Synopsis |
|---|---|
|
internal
|
|
|
tinygosmoke
command
Command tinygosmoke is the check CI runs in headless Chrome: this module compiled by TinyGo for the browser (`tinygo build -target=wasm`), with the core linked in from staticlib/wasm, loaded by index.html beside it through TinyGo's own wasm_exec.js.
|
Command tinygosmoke is the check CI runs in headless Chrome: this module compiled by TinyGo for the browser (`tinygo build -target=wasm`), with the core linked in from staticlib/wasm, loaded by index.html beside it through TinyGo's own wasm_exec.js. |