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

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 timestamp(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