AK: Improve floating point decimals formatting

There were 2 issues with the way we formatted floating point decimals:
if the part after the decimal point exceeded the max of an u64 we would
generate wildly incorrect decimals, and we applied no rounding.

With this new code, we emit decimals one by one and perform a simple
reverse string walk to round the number up if required.
This commit is contained in:
Jelle Raaijmakers 2023-10-15 16:58:30 +02:00 committed by Tim Flynn
parent f8763c16b2
commit b015926f8e
Notes: sideshowbarker 2024-07-17 06:45:52 +09:00
3 changed files with 81 additions and 40 deletions

View file

@ -447,6 +447,29 @@ ErrorOr<void> FormatBuilder::put_fixed_point(
}
#ifndef KERNEL
static ErrorOr<void> round_up_digits(StringBuilder& digits_builder)
{
auto digits_buffer = TRY(digits_builder.to_byte_buffer());
int current_position = digits_buffer.size() - 1;
while (current_position >= 0) {
if (digits_buffer[current_position] == '.') {
--current_position;
continue;
}
++digits_buffer[current_position];
if (digits_buffer[current_position] <= '9')
break;
digits_buffer[current_position] = '0';
--current_position;
}
digits_builder.clear();
if (current_position < 0)
TRY(digits_builder.try_append('1'));
return digits_builder.try_append(digits_buffer);
}
ErrorOr<void> FormatBuilder::put_f64(
double value,
u8 base,
@ -484,37 +507,40 @@ ErrorOr<void> FormatBuilder::put_f64(
value = -value;
TRY(format_builder.put_u64(static_cast<u64>(value), base, false, upper_case, false, use_separator, Align::Right, 0, ' ', sign_mode, is_negative));
value -= static_cast<i64>(value);
if (precision > 0) {
// FIXME: This is a terrible approximation but doing it properly would be a lot of work. If someone is up for that, a good
// place to start would be the following video from CppCon 2019:
// https://youtu.be/4P_kbF0EbZM (Stephan T. Lavavej “Floating-Point <charconv>: Making Your Code 10x Faster With C++17's Final Boss”)
value -= static_cast<i64>(value);
double epsilon = 0.5;
for (size_t i = 0; i < precision; ++i)
epsilon /= 10.0;
size_t visible_precision = 0;
for (; visible_precision < precision; ++visible_precision) {
if (value - static_cast<i64>(value) < epsilon && display_mode != RealNumberDisplayMode::FixedPoint)
break;
value *= 10.0;
epsilon *= 10.0;
if (!zero_pad && display_mode != RealNumberDisplayMode::FixedPoint) {
for (size_t i = 0; i < precision; ++i)
epsilon /= 10.0;
}
if (zero_pad || visible_precision > 0)
TRY(string_builder.try_append('.'));
for (size_t digit = 0; digit < precision; ++digit) {
if (!zero_pad && display_mode != RealNumberDisplayMode::FixedPoint && value - static_cast<i64>(value) < epsilon)
break;
if (visible_precision > 0)
TRY(format_builder.put_u64(static_cast<u64>(value), base, false, upper_case, true, false, Align::Right, visible_precision));
value *= 10.0;
epsilon *= 10.0;
if (zero_pad && (precision - visible_precision) > 0)
TRY(format_builder.put_u64(0, base, false, false, true, false, Align::Right, precision - visible_precision));
if (value > NumericLimits<u32>::max())
value -= static_cast<u64>(value) - (static_cast<u64>(value) % 10);
if (digit == 0)
TRY(string_builder.try_append('.'));
TRY(string_builder.try_append('0' + (static_cast<u32>(value) % 10)));
}
}
TRY(put_string(string_builder.string_view(), align, min_width, NumericLimits<size_t>::max(), fill));
return {};
// Round up if the following decimal is 5 or higher
if (static_cast<u64>(value * 10.0) % 10 >= 5)
TRY(round_up_digits(string_builder));
return put_string(string_builder.string_view(), align, min_width, NumericLimits<size_t>::max(), fill);
}
ErrorOr<void> FormatBuilder::put_f80(
@ -553,31 +579,39 @@ ErrorOr<void> FormatBuilder::put_f80(
value = -value;
TRY(format_builder.put_u64(static_cast<u64>(value), base, false, upper_case, false, use_separator, Align::Right, 0, ' ', sign_mode, is_negative));
value -= static_cast<i64>(value);
if (precision > 0) {
// FIXME: This is a terrible approximation but doing it properly would be a lot of work. If someone is up for that, a good
// place to start would be the following video from CppCon 2019:
// https://youtu.be/4P_kbF0EbZM (Stephan T. Lavavej “Floating-Point <charconv>: Making Your Code 10x Faster With C++17's Final Boss”)
value -= static_cast<i64>(value);
long double epsilon = 0.5l;
for (size_t i = 0; i < precision; ++i)
epsilon /= 10.0l;
if (display_mode != RealNumberDisplayMode::FixedPoint) {
for (size_t i = 0; i < precision; ++i)
epsilon /= 10.0l;
}
size_t visible_precision = 0;
for (; visible_precision < precision; ++visible_precision) {
if (value - static_cast<i64>(value) < epsilon && display_mode != RealNumberDisplayMode::FixedPoint)
for (size_t digit = 0; digit < precision; ++digit) {
if (display_mode != RealNumberDisplayMode::FixedPoint && value - static_cast<i64>(value) < epsilon)
break;
value *= 10.0l;
epsilon *= 10.0l;
}
if (visible_precision > 0) {
string_builder.append('.');
TRY(format_builder.put_u64(static_cast<u64>(value), base, false, upper_case, true, false, Align::Right, visible_precision));
if (value > NumericLimits<u32>::max())
value -= static_cast<u64>(value) - (static_cast<u64>(value) % 10);
if (digit == 0)
TRY(string_builder.try_append('.'));
TRY(string_builder.try_append('0' + (static_cast<u32>(value) % 10)));
}
}
// Round up if the following decimal is 5 or higher
if (static_cast<u64>(value * 10.0l) % 10 >= 5)
TRY(round_up_digits(string_builder));
TRY(put_string(string_builder.string_view(), align, min_width, NumericLimits<size_t>::max(), fill));
return {};
}

View file

@ -262,6 +262,18 @@ TEST_CASE(floating_point_numbers)
EXPECT_EQ(DeprecatedString::formatted("{:'.4}", 1234.5678), "1,234.5678");
EXPECT_EQ(DeprecatedString::formatted("{:'.4}", -1234.5678), "-1,234.5678");
EXPECT_EQ(DeprecatedString::formatted("{:.30f}", 1.0), "1.000000000000000000000000000000");
EXPECT_EQ(DeprecatedString::formatted("{:.30f}", 1.5), "1.500000000000000000000000000000");
EXPECT_EQ(DeprecatedString::formatted("{:.30f}", -2.0), "-2.000000000000000000000000000000");
EXPECT_EQ(DeprecatedString::formatted("{:.0f}", 1.4), "1");
EXPECT_EQ(DeprecatedString::formatted("{:.0f}", 1.5), "2");
EXPECT_EQ(DeprecatedString::formatted("{:.0f}", -1.9), "-2");
EXPECT_EQ(DeprecatedString::formatted("{:.1f}", 1.4), "1.4");
EXPECT_EQ(DeprecatedString::formatted("{:.1f}", 1.99), "2.0");
EXPECT_EQ(DeprecatedString::formatted("{:.1f}", 9.999), "10.0");
EXPECT_EQ(DeprecatedString::formatted("{}", NAN), "nan");
EXPECT_EQ(DeprecatedString::formatted("{}", INFINITY), "inf");
EXPECT_EQ(DeprecatedString::formatted("{}", -INFINITY), "-inf");
@ -276,11 +288,6 @@ TEST_CASE(no_precision_no_trailing_number)
EXPECT_EQ(DeprecatedString::formatted("{:.0}", 0.1), "0");
}
TEST_CASE(yay_this_implementation_sucks)
{
EXPECT_EQ(DeprecatedString::formatted("{:.0}", .99999999999), "0");
}
TEST_CASE(precision_with_trailing_zeros)
{
EXPECT_EQ(DeprecatedString::formatted("{:0.3}", 1.12), "1.120");

View file

@ -8,10 +8,10 @@ translateY(1%) => matrix(1, 0, 0, 1, 0, 0)
scale(1, 2) => matrix(1, 0, 0, 2, 0, 0)
scaleX(2) => matrix(2, 0, 0, 1, 0, 0)
scaleY(2.5) => matrix(1, 0, 0, 2.5, 0, 0)
rotate(1deg) => matrix(0.999847, 0.017452, -0.017452, 0.999847, 0, 0)
rotateX(1rad) => matrix3d(1, 0, 0, 0, 0, 0.540302, 0.841470, 0, 0, -0.841470, 0.540302, 0, 0, 0, 0, 1)
rotateY(1grad) => matrix3d(0.999876, 0, -0.015707, 0, 0, 1, 0, 0, 0.015707, 0, 0.999876, 0, 0, 0, 0, 1)
rotate(1deg) => matrix(0.999848, 0.017452, -0.017452, 0.999848, 0, 0)
rotateX(1rad) => matrix3d(1, 0, 0, 0, 0, 0.540302, 0.841471, 0, 0, -0.841471, 0.540302, 0, 0, 0, 0, 1)
rotateY(1grad) => matrix3d(0.999877, 0, -0.015707, 0, 0, 1, 0, 0, 0.015707, 0, 0.999877, 0, 0, 0, 0, 1)
rotateZ(1turn) => matrix(1, 0, -0, 1, 0, 0)
skew(1deg, 1rad) => matrix(1, 1.557407, 0.017455, 1, 0, 0)
skew(1deg, 1rad) => matrix(1, 1.557408, 0.017455, 1, 0, 0)
skewX(1deg) => matrix(1, 0, 0.017455, 1, 0, 0)
skewY(1rad) => matrix(1, 1.557407, 0, 1, 0, 0)
skewY(1rad) => matrix(1, 1.557408, 0, 1, 0, 0)