Skip to content

Commit

Permalink
LibCrypto: Improve precision of Crypto::BigFraction::to_double()
Browse files Browse the repository at this point in the history
Before:
 - FIXME: very naive implementation
 - was preventing passing some Temporal tests
   - https://github.com/tc39/test262
   - https://github.com/LadybirdBrowser/libjs-test262
  • Loading branch information
manuel-za committed Jan 9, 2025
1 parent 2ce3ad5 commit b895ca2
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 2 deletions.
62 changes: 60 additions & 2 deletions Libraries/LibCrypto/BigFraction/BigFraction.cpp
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
/*
* Copyright (c) 2022, Lucas Chollet <[email protected]>
* Copyright (c) 2025, Manuel Zahariev <[email protected]>
*
* SPDX-License-Identifier: BSD-2-Clause
*/

#include "BigFraction.h"
#include "LibCrypto/BigInt/UnsignedBigInteger.h"
#include <AK/ByteString.h>
#include <AK/Math.h>
#include <AK/StringBuilder.h>
#include <LibCrypto/NumberTheory/ModularFunctions.h>
#include <cstddef>
#include <limits>

namespace Crypto {

Expand Down Expand Up @@ -134,10 +138,64 @@ BigFraction::BigFraction(double d)
m_numerator.set_to(negative ? (m_numerator.negated_value()) : m_numerator);
}

/*
* Complexity O(N), where N=total number of words in numerator and denominator:
* - shifts: O(N)
* - division: constant (fixed bound on size of shifted numerator and denominator
* - conversion to double: constant (64-bit quotient)
*/
double BigFraction::to_double() const
{
// FIXME: very naive implementation
return m_numerator.to_double() / m_denominator.to_double();
// 1. Shift the numerator and denominator so that:
// - the denominator is at most 64 bits
// - the numerator is exactly 64 bits larger than the denominator
size_t const bit_precision = 64; // divide the fraction to this precision
bool const sign = m_numerator.is_negative();

int denominator_right_shift = 0;

size_t bit_size_numerator = m_numerator.unsigned_value().one_based_index_of_highest_set_bit();
size_t bit_size_denominator = m_denominator.one_based_index_of_highest_set_bit();

if (bit_size_denominator > bit_precision) { // reduce precision of a large denominator
denominator_right_shift = bit_size_denominator - bit_precision;
bit_size_denominator = bit_precision;
}

int numerator_right_shift = bit_size_numerator - bit_size_denominator - bit_precision;

UnsignedBigInteger shifted_denominator = m_denominator.shift_right(denominator_right_shift);
UnsignedBigInteger shifted_numerator;
if (numerator_right_shift < 0)
shifted_numerator.set_to(m_numerator.unsigned_value().shift_left(-numerator_right_shift)); // increase numerator to increase precision
else
shifted_numerator.set_to(m_numerator.unsigned_value().shift_right(numerator_right_shift)); // decrease precision of numerator

// 2. Divide the shifted numerator to the shifted denominator. The result will have 64-bit precision.
// Then, convert the quotient to double.
double result = SignedBigInteger { shifted_numerator.divided_by(shifted_denominator).quotient, sign }.to_double();

// 3. Convert the result to a double, including the denominator/numerator shifts in the exponent.
using Extractor = FloatExtractor<double>;

Extractor double_extractor;
double_extractor.d = result;

int exponent = double_extractor.exponent + numerator_right_shift - denominator_right_shift;

if ((exponent < 0) && sign) // undeflow
return -0.0;
if (exponent < 0)
return +0.0;
if ((exponent > int(Extractor::exponent_max)) && sign) // overflow
return -std::numeric_limits<double>::infinity();
if (exponent > int(Extractor::exponent_max))
return std::numeric_limits<double>::infinity();

double_extractor.sign = sign;
double_extractor.exponent += (numerator_right_shift - denominator_right_shift);

return double_extractor.d;
}

bool BigFraction::is_zero() const
Expand Down
40 changes: 40 additions & 0 deletions Tests/LibCrypto/TestBigFraction.cpp
Original file line number Diff line number Diff line change
@@ -1,12 +1,28 @@
/*
* Copyright (c) 2024, Tim Ledbetter <[email protected]>
* Copyright (c) 2025, Manuel Zahariev <[email protected]>
*
* SPDX-License-Identifier: BSD-2-Clause
*/

#include <LibCrypto/BigFraction/BigFraction.h>
#include <LibCrypto/BigInt/SignedBigInteger.h>
#include <LibCrypto/BigInt/UnsignedBigInteger.h>
#include <LibTest/Macros.h>
#include <LibTest/TestCase.h>

static Crypto::UnsignedBigInteger bigint_fibonacci(size_t n)
{
Crypto::UnsignedBigInteger num1(0);
Crypto::UnsignedBigInteger num2(1);
for (size_t i = 0; i < n; ++i) {
Crypto::UnsignedBigInteger t = num1.plus(num2);
num2 = num1;
num1 = t;
}
return num1;
}

TEST_CASE(roundtrip_from_string)
{
Array valid_number_strings {
Expand All @@ -26,3 +42,27 @@ TEST_CASE(roundtrip_from_string)
EXPECT_EQ(result.to_string(precision), valid_number_string);
}
}

TEST_CASE(big_fraction_to_double)
{
// Golden ratio:
// - limit (inf) ratio of two consecutive fibonacci numbers
// - also ( 1 + sqrt( 5 ))/2
Crypto::BigFraction phi(Crypto::SignedBigInteger { bigint_fibonacci(500) }, bigint_fibonacci(499));
// Power 64 of golden ratio:
// - limit ratio of two 64-separated fibonacci numbers
// - also (23725150497407 + 10610209857723 * sqrt( 5 ))/2
Crypto::BigFraction phi_64(Crypto::SignedBigInteger { bigint_fibonacci(564) }, bigint_fibonacci(500));

EXPECT_EQ(phi.to_double(), 1.618033988749895); // 1.6180339887498948482045868343656381177203091798057628621... (https://oeis.org/A001622)
EXPECT_EQ(phi_64.to_double(), 23725150497407); // 23725150497406.9999999999999578506361799772097881088769... (https://www.calculator.net/big-number-calculator.html)
}

TEST_CASE(big_fraction_temporal_duration_precision_support)
{
// https://github.com/tc39/test262/blob/main/test/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-1.js
// Express 4000h and 1ns in hours, as a double
Crypto::BigFraction temporal_duration_precision_test = Crypto::BigFraction { Crypto::SignedBigInteger { "14400000000000001"_bigint }, "3600000000000"_bigint };

EXPECT_EQ(temporal_duration_precision_test.to_double(), 4000.0000000000005);
}

0 comments on commit b895ca2

Please sign in to comment.