Skip to content

Commit

Permalink
Implement sprintf "%e" in Crystal (crystal-lang#14084)
Browse files Browse the repository at this point in the history
  • Loading branch information
HertzDevil authored Dec 15, 2023
1 parent 1c8a68a commit a349157
Show file tree
Hide file tree
Showing 2 changed files with 213 additions and 4 deletions.
176 changes: 174 additions & 2 deletions spec/std/sprintf_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -407,10 +407,182 @@ describe "::sprintf" do
end
end

context "scientific format" do
it "works" do
assert_sprintf "%e", 123.45, "1.234500e+2"
assert_sprintf "%E", 123.45, "1.234500E+2"

assert_sprintf "%e", Float64::MAX, "1.797693e+308"
assert_sprintf "%e", Float64::MIN_POSITIVE, "2.225074e-308"
assert_sprintf "%e", Float64::MIN_SUBNORMAL, "4.940656e-324"
assert_sprintf "%e", 0.0, "0.000000e+0"
assert_sprintf "%e", -0.0, "-0.000000e+0"
assert_sprintf "%e", -Float64::MIN_SUBNORMAL, "-4.940656e-324"
assert_sprintf "%e", -Float64::MIN_POSITIVE, "-2.225074e-308"
assert_sprintf "%e", Float64::MIN, "-1.797693e+308"
end

context "width specifier" do
it "sets the minimum length of the string" do
assert_sprintf "%20e", 123.45, " 1.234500e+2"
assert_sprintf "%20e", -123.45, " -1.234500e+2"
assert_sprintf "%+20e", 123.45, " +1.234500e+2"

assert_sprintf "%12e", 123.45, " 1.234500e+2"
assert_sprintf "%12e", -123.45, "-1.234500e+2"
assert_sprintf "%+12e", 123.45, "+1.234500e+2"

assert_sprintf "%11e", 123.45, "1.234500e+2"
assert_sprintf "%11e", -123.45, "-1.234500e+2"
assert_sprintf "%+11e", 123.45, "+1.234500e+2"

assert_sprintf "%2e", 123.45, "1.234500e+2"
assert_sprintf "%2e", -123.45, "-1.234500e+2"
assert_sprintf "%+2e", 123.45, "+1.234500e+2"
end

it "left-justifies on negative width" do
assert_sprintf "%*e", [-20, 123.45], "1.234500e+2 "
end
end

context "precision specifier" do
it "sets the minimum length of the fractional part" do
assert_sprintf "%.0e", 2.0, "2e+0"
assert_sprintf "%.0e", 2.5.prev_float, "2e+0"
assert_sprintf "%.0e", 2.5, "2e+0"
assert_sprintf "%.0e", 2.5.next_float, "3e+0"
assert_sprintf "%.0e", 3.0, "3e+0"
assert_sprintf "%.0e", 3.5.prev_float, "3e+0"
assert_sprintf "%.0e", 3.5, "4e+0"
assert_sprintf "%.0e", 3.5.next_float, "4e+0"
assert_sprintf "%.0e", 4.0, "4e+0"

assert_sprintf "%.0e", 9.5, "1e+1"

assert_sprintf "%.100e", 1.1, "1.1000000000000000888178419700125232338905334472656250000000000000000000000000000000000000000000000000e+0"

assert_sprintf "%.10000e", 1.0, "1.#{"0" * 10000}e+0"

assert_sprintf "%.1000e", Float64::MIN_POSITIVE.prev_float,
"2.2250738585072008890245868760858598876504231122409594654935248025624400092282356951" \
"787758888037591552642309780950434312085877387158357291821993020294379224223559819827" \
"501242041788969571311791082261043971979604000454897391938079198936081525613113376149" \
"842043271751033627391549782731594143828136275113838604094249464942286316695429105080" \
"201815926642134996606517803095075913058719846423906068637102005108723282784678843631" \
"944515866135041223479014792369585208321597621066375401613736583044193603714778355306" \
"682834535634005074073040135602968046375918583163124224521599262546494300836851861719" \
"422417646455137135420132217031370496583210154654068035397417906022589503023501937519" \
"773030945763173210852507299305089761582519159720757232455434770912461317493580281734" \
"466552734375000000000000000000000000000000000000000000000000000000000000000000000000" \
"000000000000000000000000000000000000000000000000000000000000000000000000000000000000" \
"000000000000000000000000000000000000000000000000000000000000000000000000000000e-308"
end

it "can be used with width" do
assert_sprintf "%20.12e", 123.45, " 1.234500000000e+2"
assert_sprintf "%20.12e", -123.45, " -1.234500000000e+2"
assert_sprintf "%20.12e", 0.0, " 0.000000000000e+0"

assert_sprintf "%-20.12e", 123.45, "1.234500000000e+2 "
assert_sprintf "%-20.12e", -123.45, "-1.234500000000e+2 "
assert_sprintf "%-20.12e", 0.0, "0.000000000000e+0 "

assert_sprintf "%8.12e", 123.45, "1.234500000000e+2"
assert_sprintf "%8.12e", -123.45, "-1.234500000000e+2"
assert_sprintf "%8.12e", 0.0, "0.000000000000e+0"
end

it "is ignored if precision argument is negative" do
assert_sprintf "%.*e", [-2, 123.45], "1.234500e+2"
end
end

context "sharp flag" do
it "prints a decimal point even if no digits follow" do
assert_sprintf "%#.0e", 1.0, "1.e+0"
assert_sprintf "%#.0e", 10000.0, "1.e+4"
assert_sprintf "%#.0e", 1.0e+23, "1.e+23"
assert_sprintf "%#.0e", 1.0e-100, "1.e-100"
assert_sprintf "%#.0e", 0.0, "0.e+0"
assert_sprintf "%#.0e", -0.0, "-0.e+0"
end
end

context "plus flag" do
it "writes a plus sign for positive values" do
assert_sprintf "%+e", 123.45, "+1.234500e+2"
assert_sprintf "%+e", -123.45, "-1.234500e+2"
assert_sprintf "%+e", 0.0, "+0.000000e+0"
end

it "writes plus sign after left space-padding" do
assert_sprintf "%+20e", 123.45, " +1.234500e+2"
assert_sprintf "%+20e", -123.45, " -1.234500e+2"
assert_sprintf "%+20e", 0.0, " +0.000000e+0"
end

it "writes plus sign before left zero-padding" do
assert_sprintf "%+020e", 123.45, "+000000001.234500e+2"
assert_sprintf "%+020e", -123.45, "-000000001.234500e+2"
assert_sprintf "%+020e", 0.0, "+000000000.000000e+0"
end
end

context "space flag" do
it "writes a space for positive values" do
assert_sprintf "% e", 123.45, " 1.234500e+2"
assert_sprintf "% e", -123.45, "-1.234500e+2"
assert_sprintf "% e", 0.0, " 0.000000e+0"
end

it "writes space before left space-padding" do
assert_sprintf "% 20e", 123.45, " 1.234500e+2"
assert_sprintf "% 20e", -123.45, " -1.234500e+2"
assert_sprintf "% 20e", 0.0, " 0.000000e+0"

assert_sprintf "% 020e", 123.45, " 000000001.234500e+2"
assert_sprintf "% 020e", -123.45, "-000000001.234500e+2"
assert_sprintf "% 020e", 0.0, " 000000000.000000e+0"
end

it "is ignored if plus flag is also specified" do
assert_sprintf "% +e", 123.45, "+1.234500e+2"
assert_sprintf "%+ e", -123.45, "-1.234500e+2"
end
end

context "zero flag" do
it "left-pads the result with zeros" do
assert_sprintf "%020e", 123.45, "0000000001.234500e+2"
assert_sprintf "%020e", -123.45, "-000000001.234500e+2"
assert_sprintf "%020e", 0.0, "0000000000.000000e+0"
end

it "is ignored if string is left-justified" do
assert_sprintf "%-020e", 123.45, "1.234500e+2 "
assert_sprintf "%-020e", -123.45, "-1.234500e+2 "
assert_sprintf "%-020e", 0.0, "0.000000e+0 "
end

it "can be used with precision" do
assert_sprintf "%020.12e", 123.45, "0001.234500000000e+2"
assert_sprintf "%020.12e", -123.45, "-001.234500000000e+2"
assert_sprintf "%020.12e", 0.0, "0000.000000000000e+0"
end
end

context "minus flag" do
it "left-justifies the string" do
assert_sprintf "%-20e", 123.45, "1.234500e+2 "
assert_sprintf "%-20e", -123.45, "-1.234500e+2 "
assert_sprintf "%-20e", 0.0, "0.000000e+0 "
end
end
end

pending_win32 "works for other formats" do
assert_sprintf "%g", 123, "123"
assert_sprintf "%e", 123.45, "1.234500e+02"
assert_sprintf "%E", 123.45, "1.234500E+02"
assert_sprintf "%G", 12345678.45, "1.23457E+07"
assert_sprintf "%a", 12345678.45, "0x1.78c29ce666666p+23"
assert_sprintf "%A", 12345678.45, "0X1.78C29CE666666P+23"
Expand Down
41 changes: 39 additions & 2 deletions src/string/formatter.cr
Original file line number Diff line number Diff line change
Expand Up @@ -387,8 +387,45 @@ struct String::Formatter(A)

# Formats floats with `%e` or `%E`
private def float_scientific(float, flags)
# TODO: implement using `Float::Printer::RyuPrintf`
float_fallback(float, flags)
# the longest string possible is due to
# `Float64::MIN_POSITIVE.prev_float`, which produces `2.` followed by 766
# nonzero digits and then `e-308`; there is also no need for any precision
# > 766 because all trailing digits will be zeros
if precision = flags.precision
printf_precision = {precision.to_u32, 766_u32}.min
trailing_zeros = {precision - printf_precision, 0}.max
else
# default precision for C's `%e`
printf_precision = 6_u32
trailing_zeros = 0
end

printf_buf = uninitialized UInt8[773]
printf_size = Float::Printer::RyuPrintf.d2exp_buffered_n(float, printf_precision, printf_buf.to_unsafe)
printf_slice = printf_buf.to_slice[0, printf_size]
dot_index = printf_slice.index('.'.ord)
e_index = printf_slice.rindex!('e'.ord)
sign = Math.copysign(1.0, float)

printf_slice[e_index] = 'E'.ord.to_u8! if flags.type == 'E'

str_size = printf_size + trailing_zeros
str_size += 1 if sign < 0 || flags.plus || flags.space
str_size += 1 if flags.sharp && dot_index.nil?

pad(str_size, flags) if flags.left_padding? && flags.padding_char != '0'

# this preserves -0.0's sign correctly
write_plus_or_space(sign, flags)
@io << '-' if sign < 0

pad(str_size, flags) if flags.left_padding? && flags.padding_char == '0'
@io.write_string(printf_slice[0, e_index])
trailing_zeros.times { @io << '0' }
@io << '.' if flags.sharp && dot_index.nil?
@io.write_string(printf_slice[e_index..])

pad(str_size, flags) if flags.right_padding?
end

# Formats floats with `%g` or `%G`
Expand Down

0 comments on commit a349157

Please sign in to comment.