Module: HyperCast
- Defined in:
- lib/hypercast.rb,
lib/hypercast/runtime.rb,
lib/hypercast/native_platform.rb
Overview
Allocation-lean scalar casts — booleans, numerics, UUIDs, temporals — calling directly into the native libhypercast shared library via Fiddle. Every door returns a verdict: Success or Fault (a closed reason plus the offending byte span), never an exception for bad data — the only exceptions here are caller bugs (a malformed NumFormat), never data.
Consume with Ruby's own pattern matching over the two Data case types:
case HyperCast.i32("(1,234)", HyperCast::NumFormat::INVARIANT)
in HyperCast::Success(value:) then puts "got #{value}" # -1234
in HyperCast::Fault(reason:, offset:) then puts "#{reason} at byte #{offset}"
end
Door names mirror the native ABI (i32, f64, timestamp, ...) so the polyglot surface reads identically across bindings. Ruby-flavored fidelity: Integer is unbounded (u64 comes back as the true unsigned value), Time carries full nanoseconds across the whole 0001-9999 window, time-of-day is an exact Integer of nanoseconds since midnight, and durations come back as exact Rational seconds — no truncation anywhere, and no wrapping: Ruby and the JVM are the fidelity kings of this roster.
Defined Under Namespace
Modules: NativePlatform, Runtime Classes: Fault, NumFormat, Success
Constant Summary collapse
- VERSION =
This gem's own version — kept in lockstep with hypercast.gemspec by the prepare-release workflow, so the two can never drift apart again.
"0.0.2"- REASONS =
The native core's failure codes, mapped to the closed reason Symbols a Fault carries.
{ 1 => :empty, 2 => :malformed, 3 => :out_of_range }.freeze
- GROUPING =
Permit the group separator between digits (sizes not validated — between digits is the rule).
1- PARENTHESES =
Permit accounting parentheses as negation: (1,234) is -1234.
1 << 1
- EXPONENT =
Permit exponent notation. Integer doors reject a negative exponent.
1 << 2
- RADIX_PREFIXES =
Permit 0x/&H/0b two's-complement radix prefixes (0xFF is -1 for an i8).
1 << 3
- PERCENT =
Permit a trailing %, dividing by 100. Real doors only.
1 << 4
- SEPARATOR_DETECT =
Resolve 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 a :malformed Fault at the separator, never guessed.
1 << 5
- ALL_STYLES =
Every lenience on (SEPARATOR_DETECT is a separator policy, not a lenience, and is deliberately not included).
GROUPING | PARENTHESES | EXPONENT | RADIX_PREFIXES | PERCENT
- UNIX_PRECISIONS =
The declared unit of a Unix-epoch value — no magnitude guessing, ever.
{ seconds: 1, milliseconds: 2, microseconds: 3, nanoseconds: 4 }.freeze
- EXCEL_EPOCHS =
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 UNIX_PRECISIONS and DATE_ORDERS are declared rather than guessed.
{ y1900: 1, y1904: 2 }.freeze
- DATE_ORDERS =
The declared field order of a separated calendar date — no guessing, ever: "1/7/2026" is January 7th (:month_day_year, the en-US order) or July 1st (:day_month_year, the en-GB order) only because the caller said which.
{ year_month_day: 1, month_day_year: 2, day_month_year: 3 }.freeze
- BACKEND =
--- backend selection: the Magnus extension, when present, replaces the doors above in place on this module (no delegation layer) — Fiddle's measured 1.6 µs per-call floor drops to an ordinary extension call. The pure-Fiddle definitions stay the universal zero-compile fallback; precompiled platform gems are how the extension ships without ever making a consumer compile anything. Set HYPERCAST_PURE=1 to force Fiddle.
if ENV["HYPERCAST_PURE"] :fiddle else begin require "hypercast_native" :native rescue LoadError :fiddle end end
Class Method Summary collapse
-
.bool(text) ⇒ Object
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.
-
.date(text, order = nil) ⇒ Object
Casts a calendar date to a Date.
-
.datetime(text, order) ⇒ Object
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 a declared order Symbol to a stdlib DateTime with exact Rational seconds.
-
.duration(text) ⇒ Object
Casts a duration (ISO 8601 fixed components, invariant colon form, or protobuf JSON seconds) to exact Rational seconds — full fidelity across the core's ±10,000-year window, no wrapping and no truncation.
-
.excel_serial(text, epoch) ⇒ Object
Casts an Excel date serial under a caller-declared epoch Symbol (:y1900/:y1904) to a UTC Time.
-
.f32(text, format) ⇒ Object
Casts real text to an IEEE single (widened losslessly on the way out): finite values only, declared separators and grouping, parens, exponent, and trailing percent.
-
.f64(text, format) ⇒ Object
Casts real text to an IEEE double.
-
.optional(verdict) ⇒ Object
Presents a verdict optionally: an :empty fault becomes nil (Ruby's absent), everything else flows through untouched.
-
.time(text) ⇒ Object
Casts an ISO 24-hour time-of-day to an exact Integer of nanoseconds since midnight (Ruby has no time-of-day type; the integer keeps every digit).
-
.timestamp(text) ⇒ Object
Casts an RFC 3339 instant — zone mandatory — to a UTC Time at full nanosecond fidelity across the whole 0001-9999 window.
-
.unix(text, precision) ⇒ Object
Casts an integer Unix-epoch value under a caller-declared unit Symbol (:seconds/:milliseconds/:microseconds/:nanoseconds) to a UTC Time.
-
.uuid(text) ⇒ Object
Casts UUID text — all five .NET Guid formats (D/N/B/P/X) plus urn:uuid:/GUID:/UUID: prefixes — to Ruby's UUID lingua franca: the lowercase hyphenated String (the same shape SecureRandom.uuid returns).
Class Method Details
.bool(text) ⇒ Object
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.
115 116 117 |
# File 'lib/hypercast.rb', line 115 def bool(text) plain(:cast_bool, text, 1) { |out| out.unpack1("C") != 0 } end |
.date(text, order = nil) ⇒ Object
Casts a calendar date to a Date. With no order declared: the strict ISO 8601 yyyy-MM-dd form only. With a declared order Symbol (:year_month_day, :month_day_year, :day_month_year), also the separated forms — "1/7/2026" is January 7th or July 1st only because the caller said which; an unknown order is a caller bug (KeyError), never a verdict.
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 |
# File 'lib/hypercast.rb', line 191 def date(text, order = nil) if order.nil? plain(:cast_date, text, 4) do |out| year, month, day = out.unpack("S<CC") Date.new(year, month, day) end else code = DATE_ORDERS.fetch(order) bytes = utf8(text) out, fault, = scratch rc = Runtime.call(:cast_date_ordered, input_ptr(bytes), bytes.bytesize, code, out, fault) verdict(rc, fault) do year, month, day = out[0, 4].unpack("S<CC") Date.new(year, month, day) end end end |
.datetime(text, order) ⇒ Object
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 a declared order Symbol to a stdlib DateTime with exact Rational seconds. No zone is read: the text names no instant, and the parse applies no offset. Ruby has no zone-less date-time type, so the value rides a DateTime, whose offset defaults to +00:00 — that zero is a carrier artifact, not data (the same caveat PHP's UTC-labeled DateTimeImmutable carries); fusing a real zone is the caller's job, and timestamp stays the strict RFC 3339 instant door. An unknown order is a caller bug (KeyError).
217 218 219 220 221 222 223 224 225 226 227 228 229 |
# File 'lib/hypercast.rb', line 217 def datetime(text, order) code = DATE_ORDERS.fetch(order) bytes = utf8(text) out, fault, = scratch rc = Runtime.call(:cast_datetime, input_ptr(bytes), bytes.bytesize, code, out, fault) verdict(rc, fault) do year, month, day, nanos = out[0, 16].unpack("S<CCx4Q<") second_of_day, frac = nanos.divmod(1_000_000_000) hour, rest = second_of_day.divmod(3600) minute, second = rest.divmod(60) DateTime.new(year, month, day, hour, minute, second + Rational(frac, 1_000_000_000)) end end |
.duration(text) ⇒ Object
Casts a duration (ISO 8601 fixed components, invariant colon form, or protobuf JSON seconds) to exact Rational seconds — full fidelity across the core's ±10,000-year window, no wrapping and no truncation.
240 241 242 243 244 245 |
# File 'lib/hypercast.rb', line 240 def duration(text) plain(:cast_duration, text, 16) do |out| seconds, nanos = out.unpack("q<l<") Rational(seconds * 1_000_000_000 + nanos, 1_000_000_000) end end |
.excel_serial(text, epoch) ⇒ Object
Casts an Excel date serial under a caller-declared epoch Symbol (:y1900/:y1904) to a UTC 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 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 :malformed here — the same verdict .date gives the text "1900-02-29" — so every serial above it is shifted one day against a naive count. An unknown epoch is a caller bug (KeyError), never a verdict.
178 179 180 181 182 183 184 |
# File 'lib/hypercast.rb', line 178 def excel_serial(text, epoch) code = EXCEL_EPOCHS.fetch(epoch) bytes = utf8(text) out, fault, = scratch rc = Runtime.call(:cast_excel_serial, input_ptr(bytes), bytes.bytesize, code, out, fault) verdict(rc, fault) { instant(out[0, 16]) } end |
.f32(text, format) ⇒ Object
Casts real text to an IEEE single (widened losslessly on the way out): finite values only, declared separators and grouping, parens, exponent, and trailing percent.
133 134 135 |
# File 'lib/hypercast.rb', line 133 def f32(text, format) numeric(:cast_f32, text, format, 4) { |out| out.unpack1("e") } end |
.f64(text, format) ⇒ Object
Casts real text to an IEEE double. Notation rules as f32.
138 139 140 |
# File 'lib/hypercast.rb', line 138 def f64(text, format) numeric(:cast_f64, text, format, 8) { |out| out.unpack1("E") } end |
.optional(verdict) ⇒ Object
Presents a verdict optionally: an :empty fault becomes nil (Ruby's absent), everything else flows through untouched.
106 107 108 109 110 |
# File 'lib/hypercast.rb', line 106 def optional(verdict) return nil if verdict in Fault(reason: :empty) verdict end |
.time(text) ⇒ Object
Casts an ISO 24-hour time-of-day to an exact Integer of nanoseconds since midnight (Ruby has no time-of-day type; the integer keeps every digit).
233 234 235 |
# File 'lib/hypercast.rb', line 233 def time(text) plain(:cast_time, text, 8) { |out| out.unpack1("Q<") } end |
.timestamp(text) ⇒ Object
Casts an RFC 3339 instant — zone mandatory — to a UTC Time at full nanosecond fidelity across the whole 0001-9999 window.
153 154 155 |
# File 'lib/hypercast.rb', line 153 def (text) plain(:cast_timestamp, text, 16) { |out| instant(out) } end |
.unix(text, precision) ⇒ Object
Casts an integer Unix-epoch value under a caller-declared unit Symbol (:seconds/:milliseconds/:microseconds/:nanoseconds) to a UTC Time. An unknown unit is a caller bug (KeyError), never a verdict.
160 161 162 163 164 165 166 |
# File 'lib/hypercast.rb', line 160 def unix(text, precision) code = UNIX_PRECISIONS.fetch(precision) bytes = utf8(text) out, fault, = scratch rc = Runtime.call(:cast_unix, input_ptr(bytes), bytes.bytesize, code, out, fault) verdict(rc, fault) { instant(out[0, 16]) } end |
.uuid(text) ⇒ Object
Casts UUID text — all five .NET Guid formats (D/N/B/P/X) plus urn:uuid:/GUID:/UUID: prefixes — to Ruby's UUID lingua franca: the lowercase hyphenated String (the same shape SecureRandom.uuid returns).
145 146 147 148 149 |
# File 'lib/hypercast.rb', line 145 def uuid(text) plain(:cast_uuid, text, 16) do |out| out.unpack("H8H4H4H4H12").join("-") end end |