Skip to content

Commit

Permalink
Add Float::Primitive.parse_hexfloat, .parse_hexfloat?, `#to_hexfl…
Browse files Browse the repository at this point in the history
…oat` (crystal-lang#14027)

Co-authored-by: Johannes Müller <[email protected]>
  • Loading branch information
HertzDevil and straight-shoota authored Dec 7, 2023
1 parent c051c81 commit 413f2fd
Show file tree
Hide file tree
Showing 8 changed files with 1,209 additions and 155 deletions.
3 changes: 2 additions & 1 deletion spec/interpreter_std_spec.cr

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

709 changes: 709 additions & 0 deletions spec/std/float_printer/hexfloat_spec.cr

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@
# * https://github.com/microsoft/STL/tree/main/tests/std/tests/P0067R5_charconv

require "spec"
require "./spec_helper"
require "../spec_helper"
require "spec/helpers/string"
require "../support/number"
require "../../support/number"

# Tests that `v.to_s` is the same as the *v* literal is written in the source
# code, except possibly omitting the `_f32` suffix for `Float32` literals.
Expand Down
76 changes: 5 additions & 71 deletions spec/support/number.cr
Original file line number Diff line number Diff line change
Expand Up @@ -83,80 +83,14 @@ end
# TODO test zero? comparisons
# TODO test <=> comparisons between types

private module HexFloatConverter(F, U)
# Converts `str`, a hexadecimal floating-point literal, to an `F`. Truncates
# all unused bits in the mantissa.
def self.to_f(str : String) : F
m = str.match(/^(-?)0x([0-9A-Fa-f]+)(?:\.([0-9A-Fa-f]+))?p([+-]?)([0-9]+)#{"_?f32" if F == Float32}$/).not_nil!

total_bits = F == Float32 ? 32 : 64
mantissa_bits = F::MANT_DIGITS - 1
exponent_bias = F::MAX_EXP - 1

is_negative = m[1] == "-"
int_part = U.new(m[2], base: 16)
frac = m[3]?.try(&.[0, (mantissa_bits + 3) // 4]) || "0"
frac_part = U.new(frac, base: 16) << (mantissa_bits - frac.size * 4)
exponent = m[5].to_i * (m[4] == "-" ? -1 : 1)

if int_part > 1
last_bit = U.zero
while int_part > 1
last_bit = frac_part & 1
frac_part |= (U.new!(1) << mantissa_bits) if int_part & 1 != 0
frac_part >>= 1
int_part >>= 1
exponent += 1
end
if last_bit != 0
frac_part += 1
if frac_part >= U.new!(1) << mantissa_bits
frac_part = U.new!(0)
int_part += 1
end
end
elsif int_part == 0
while int_part == 0
frac_part <<= 1
if frac_part >= U.new!(1) << mantissa_bits
frac_part &= ~(U::MAX << mantissa_bits)
int_part += 1
end
exponent -= 1
end
end

exponent += exponent_bias
if exponent >= exponent_bias * 2 + 1
F::INFINITY * (is_negative ? -1 : 1)
elsif exponent < -mantissa_bits
F.zero * (is_negative ? -1 : 1)
elsif exponent <= 0
f = (frac_part >> (1 - exponent)) | (int_part << (mantissa_bits - 1 + exponent))
f |= U.new!(1) << (total_bits - 1) if is_negative
f.unsafe_as(F)
else
f = frac_part
f |= U.new!(exponent) << mantissa_bits
f |= U.new!(1) << (total_bits - 1) if is_negative
f.unsafe_as(F)
end
end
end

def hexfloat_f64(str : String) : Float64
HexFloatConverter(Float64, UInt64).to_f(str)
end

def hexfloat_f32(str : String) : Float32
HexFloatConverter(Float32, UInt32).to_f(str)
end

# Calls either `Float64.parse_hexfloat` or `Float32.parse_hexfloat`. The default
# is `Float64` unless *str* ends with `_f32`, in which case that suffix is
# stripped and `Float32` is chosen.
macro hexfloat(str)
{% raise "`str` must be a StringLiteral, not #{str.class_name}" unless str.is_a?(StringLiteral) %}
{% if str.ends_with?("_f32") %}
hexfloat_f32({{ str }})
::Float32.parse_hexfloat({{ str[0...-4] }})
{% else %}
hexfloat_f64({{ str }})
::Float64.parse_hexfloat({{ str }})
{% end %}
end
140 changes: 136 additions & 4 deletions src/float.cr
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,48 @@ struct Float32
value.to_f32!
end

# Returns a `Float32` by parsing *str* as a hexadecimal-significand string, or
# `nil` if parsing fails.
#
# The string format is defined in section 5.12.3 of IEEE 754-2008, and is the
# same as the `%a` specifier for `sprintf`. Unlike e.g. `String#to_f`,
# whitespace and underscores are not allowed. Non-finite values are also
# recognized.
#
# ```
# Float32.parse_hexfloat?("0x123.456p7") # => 37282.6875_f32
# Float32.parse_hexfloat?("0x1.fffffep+127") # => Float32::MAX
# Float32.parse_hexfloat?("-inf") # => -Float32::INFINITY
# Float32.parse_hexfloat?("0x1") # => nil
# Float32.parse_hexfloat?("a.bp+3") # => nil
# ```
def self.parse_hexfloat?(str : String) : self?
Float::Printer::Hexfloat(self, UInt32).to_f(str) { nil }
end

# Returns a `Float32` by parsing *str* as a hexadecimal-significand string.
#
# The string format is defined in section 5.12.3 of IEEE 754-2008, and is the
# same as the `%a` specifier for `sprintf`. Unlike e.g. `String#to_f`,
# whitespace and underscores are not allowed. Non-finite values are also
# recognized.
#
# Raises `ArgumentError` if *str* is not a valid hexadecimal-significand
# string.
#
# ```
# Float32.parse_hexfloat("0x123.456p7") # => 37282.6875_f32
# Float32.parse_hexfloat("0x1.fffffep+127") # => Float32::MAX
# Float32.parse_hexfloat("-inf") # => -Float32::INFINITY
# Float32.parse_hexfloat("0x1") # Invalid hexfloat: expected 'p' or 'P' (ArgumentError)
# Float32.parse_hexfloat("a.bp+3") # Invalid hexfloat: expected '0' (ArgumentError)
# ```
def self.parse_hexfloat(str : String) : self
Float::Printer::Hexfloat(self, UInt32).to_f(str) do |err|
raise ArgumentError.new("Invalid hexfloat: #{err}")
end
end

Number.expand_div [Int8, UInt8, Int16, UInt16, Int32, UInt32, Int64, UInt64, Int128, UInt128], Float32
Number.expand_div [Float64], Float64

Expand Down Expand Up @@ -214,12 +256,36 @@ struct Float32

def to_s : String
String.build(22) do |buffer|
Printer.print(self, buffer)
Printer.shortest(self, buffer)
end
end

def to_s(io : IO) : Nil
Printer.print(self, io)
Printer.shortest(self, io)
end

# Returns the hexadecimal-significand representation of `self`.
#
# The string format is defined in section 5.12.3 of IEEE 754-2008, and is the
# same as the `%a` specifier for `sprintf`. The integral part of the returned
# string is `0` if `self` is subnormal, otherwise `1`. The fractional part
# contains only significant digits.
#
# ```
# 1234.0625_f32.to_hexfloat # => "0x1.3484p+10"
# Float32::MAX.to_hexfloat # => "0x1.fffffep+127"
# Float32::MIN_SUBNORMAL.to_hexfloat # => "0x0.000002p-126"
# ```
def to_hexfloat : String
# the longest `Float64` strings are of the form `-0x1.234567p-127`
String.build(16) do |buffer|
Printer.hexfloat(self, buffer)
end
end

# Writes `self`'s hexadecimal-significand representation to the given *io*.
def to_hexfloat(io : IO) : Nil
Printer.hexfloat(self, io)
end

def clone
Expand Down Expand Up @@ -276,6 +342,48 @@ struct Float64
value.to_f64!
end

# Returns a `Float32` by parsing *str* as a hexadecimal-significand string, or
# `nil` if parsing fails.
#
# The string format is defined in section 5.12.3 of IEEE 754-2008, and is the
# same as the `%a` specifier for `sprintf`. Unlike e.g. `String#to_f`,
# whitespace and underscores are not allowed. Non-finite values are also
# recognized.
#
# ```
# Float64.parse_hexfloat?("0x123.456p7") # => 37282.6875
# Float64.parse_hexfloat?("0x1.fffffep+127") # => Float64::MAX
# Float64.parse_hexfloat?("-inf") # => -Float64::INFINITY
# Float64.parse_hexfloat?("0x1") # => nil
# Float64.parse_hexfloat?("a.bp+3") # => nil
# ```
def self.parse_hexfloat?(str : String) : self?
Float::Printer::Hexfloat(self, UInt64).to_f(str) { nil }
end

# Returns a `Float32` by parsing *str* as a hexadecimal-significand string.
#
# The string format is defined in section 5.12.3 of IEEE 754-2008, and is the
# same as the `%a` specifier for `sprintf`. Unlike e.g. `String#to_f`,
# whitespace and underscores are not allowed. Non-finite values are also
# recognized.
#
# Raises `ArgumentError` if *str* is not a valid hexadecimal-significand
# string.
#
# ```
# Float64.parse_hexfloat("0x123.456p7") # => 37282.6875
# Float64.parse_hexfloat("0x1.fffffffffffffp+1023") # => Float64::MAX
# Float64.parse_hexfloat("-inf") # => -Float64::INFINITY
# Float64.parse_hexfloat("0x1") # Invalid hexfloat: expected 'p' or 'P' (ArgumentError)
# Float64.parse_hexfloat("a.bp+3") # Invalid hexfloat: expected '0' (ArgumentError)
# ```
def self.parse_hexfloat(str : String) : self
Float::Printer::Hexfloat(self, UInt64).to_f(str) do |err|
raise ArgumentError.new("Invalid hexfloat: #{err}")
end
end

Number.expand_div [Int8, UInt8, Int16, UInt16, Int32, UInt32, Int64, UInt64, Int128, UInt128], Float64
Number.expand_div [Float32], Float64

Expand Down Expand Up @@ -338,12 +446,36 @@ struct Float64
def to_s : String
# the longest `Float64` strings are of the form `-1.2345678901234567e+123`
String.build(24) do |buffer|
Printer.print(self, buffer)
Printer.shortest(self, buffer)
end
end

def to_s(io : IO) : Nil
Printer.print(self, io)
Printer.shortest(self, io)
end

# Returns the hexadecimal-significand representation of `self`.
#
# The string format is defined in section 5.12.3 of IEEE 754-2008, and is the
# same as the `%a` specifier for `sprintf`. The integral part of the returned
# string is `0` if `self` is subnormal, otherwise `1`. The fractional part
# contains only significant digits.
#
# ```
# 1234.0625.to_hexfloat # => "0x1.3484p+10"
# Float64::MAX.to_hexfloat # => "0x1.fffffffffffffp+1023"
# Float64::MIN_SUBNORMAL.to_hexfloat # => "0x0.0000000000001p-1022"
# ```
def to_hexfloat : String
# the longest `Float64` strings are of the form `-0x1.23456789abcdep-1023`
String.build(24) do |buffer|
Printer.hexfloat(self, buffer)
end
end

# Writes `self`'s hexadecimal-significand representation to the given *io*.
def to_hexfloat(io : IO) : Nil
Printer.hexfloat(self, io)
end

def clone
Expand Down
Loading

0 comments on commit 413f2fd

Please sign in to comment.