mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-12-11 17:00:37 +00:00
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:
parent
f8763c16b2
commit
b015926f8e
Notes:
sideshowbarker
2024-07-17 06:45:52 +09:00
Author: https://github.com/gmta Commit: https://github.com/SerenityOS/serenity/commit/b015926f8e Pull-request: https://github.com/SerenityOS/serenity/pull/21454
3 changed files with 81 additions and 40 deletions
|
@ -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 {};
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue