Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions src/core/json/json_value.cc
Original file line number Diff line number Diff line change
Expand Up @@ -543,8 +543,13 @@ auto JSON::operator-=(const JSON &substractive) -> JSON & {

const auto division{dividend_value / divisor_value};
Real integral = 0;
return !std::isinf(division) && !std::isnan(division) &&
std::modf(division, &integral) == 0.0;
if (!std::isinf(division) && !std::isnan(division) &&
std::modf(division, &integral) == 0.0) {
return true;
}

return Decimal::strict_from(dividend_value)
.divisible_by(Decimal::strict_from(divisor_value));
}

if (this->is_decimal() && divisor.is_decimal()) {
Expand All @@ -557,17 +562,17 @@ auto JSON::operator-=(const JSON &substractive) -> JSON & {
return this->to_decimal().divisible_by(divisor_decimal);
}

const Decimal divisor_decimal{divisor.to_real()};
return this->to_decimal().divisible_by(divisor_decimal);
return this->to_decimal().divisible_by(
Decimal::strict_from(divisor.to_real()));
}

if (this->is_integer()) {
const Decimal dividend_decimal{this->to_integer()};
return dividend_decimal.divisible_by(divisor.to_decimal());
}

const Decimal dividend_decimal{this->to_real()};
return dividend_decimal.divisible_by(divisor.to_decimal());
return Decimal::strict_from(this->to_real())
.divisible_by(divisor.to_decimal());
}

[[nodiscard]] auto
Expand Down
11 changes: 11 additions & 0 deletions src/lang/numeric/decimal.cc
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@

#include <array> // std::array
#include <cassert> // assert
#include <charconv> // std::to_chars
#include <cmath> // std::isfinite
#include <cstddef> // std::size_t
#include <cstring> // std::strlen
#include <iomanip> // std::setprecision
#include <limits> // std::numeric_limits
Expand Down Expand Up @@ -506,6 +508,15 @@ auto Decimal::negative_infinity() -> Decimal {
return result;
}

auto Decimal::strict_from(const double value) -> Decimal {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decimal::strict_from relies on assert(result.ec == std::errc{}) after std::to_chars; in non-assert builds a conversion failure could lead to constructing a Decimal from incomplete/invalid output. Consider defining/handling the failure mode (or documenting a strict precondition like “finite values only”) so callers don’t get UB/terminations unexpectedly.

Severity: medium

Other Locations
  • src/lang/numeric/include/sourcemeta/core/numeric_decimal.h:80

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

std::array<char, 64> buffer{};
const auto result{
std::to_chars(buffer.data(), buffer.data() + buffer.size(), value)};
assert(result.ec == std::errc{});
return Decimal{std::string_view{
buffer.data(), static_cast<std::size_t>(result.ptr - buffer.data())}};
}

auto Decimal::to_scientific_string() const -> std::string {
std::string result;

Expand Down
4 changes: 4 additions & 0 deletions src/lang/numeric/include/sourcemeta/core/numeric_decimal.h
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ class SOURCEMETA_CORE_NUMERIC_EXPORT Decimal {
/// Create a signaling NaN value with an optional payload
[[nodiscard]] static auto snan(std::uint64_t payload = 0) -> Decimal;

/// Create a decimal from a double by converting through its shortest
/// round-trip string representation, avoiding IEEE 754 precision artifacts
[[nodiscard]] static auto strict_from(double value) -> Decimal;

/// Create a positive infinity value
[[nodiscard]] static auto infinity() -> Decimal;

Expand Down
234 changes: 234 additions & 0 deletions test/json/json_decimal_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -878,6 +878,240 @@ TEST(JSON_decimal, divisible_by_mixed_scale_decimal_real_divisible_true) {
EXPECT_TRUE(dividend.divisible_by(divisor));
}

TEST(JSON_decimal, divisible_by_decimal_decimal_0_01_true) {
const sourcemeta::core::JSON dividend{sourcemeta::core::Decimal{"1280.32"}};
const sourcemeta::core::JSON divisor{sourcemeta::core::Decimal{"0.01"}};
EXPECT_TRUE(dividend.divisible_by(divisor));
}

TEST(JSON_decimal, divisible_by_decimal_decimal_0_01_false) {
const sourcemeta::core::JSON dividend{sourcemeta::core::Decimal{"1280.325"}};
const sourcemeta::core::JSON divisor{sourcemeta::core::Decimal{"0.01"}};
EXPECT_FALSE(dividend.divisible_by(divisor));
}

TEST(JSON_decimal, divisible_by_decimal_decimal_0_1_true) {
const sourcemeta::core::JSON dividend{sourcemeta::core::Decimal{"100.3"}};
const sourcemeta::core::JSON divisor{sourcemeta::core::Decimal{"0.1"}};
EXPECT_TRUE(dividend.divisible_by(divisor));
}

TEST(JSON_decimal, divisible_by_decimal_decimal_0_1_false) {
const sourcemeta::core::JSON dividend{sourcemeta::core::Decimal{"100.35"}};
const sourcemeta::core::JSON divisor{sourcemeta::core::Decimal{"0.1"}};
EXPECT_FALSE(dividend.divisible_by(divisor));
}

TEST(JSON_decimal, divisible_by_decimal_decimal_0_001_true) {
const sourcemeta::core::JSON dividend{sourcemeta::core::Decimal{"25.123"}};
const sourcemeta::core::JSON divisor{sourcemeta::core::Decimal{"0.001"}};
EXPECT_TRUE(dividend.divisible_by(divisor));
}

TEST(JSON_decimal, divisible_by_decimal_decimal_0_001_false) {
const sourcemeta::core::JSON dividend{sourcemeta::core::Decimal{"25.1235"}};
const sourcemeta::core::JSON divisor{sourcemeta::core::Decimal{"0.001"}};
EXPECT_FALSE(dividend.divisible_by(divisor));
}

TEST(JSON_decimal, divisible_by_decimal_decimal_0_0001_true) {
const sourcemeta::core::JSON dividend{sourcemeta::core::Decimal{"99.9999"}};
const sourcemeta::core::JSON divisor{sourcemeta::core::Decimal{"0.0001"}};
EXPECT_TRUE(dividend.divisible_by(divisor));
}

TEST(JSON_decimal, divisible_by_decimal_decimal_0_0001_false) {
const sourcemeta::core::JSON dividend{sourcemeta::core::Decimal{"99.99995"}};
const sourcemeta::core::JSON divisor{sourcemeta::core::Decimal{"0.0001"}};
EXPECT_FALSE(dividend.divisible_by(divisor));
}

TEST(JSON_decimal, divisible_by_decimal_real_0_01_true) {
const sourcemeta::core::JSON dividend{sourcemeta::core::Decimal{"1280.32"}};
const sourcemeta::core::JSON divisor{0.01};
EXPECT_TRUE(dividend.divisible_by(divisor));
}

TEST(JSON_decimal, divisible_by_decimal_real_0_01_false) {
const sourcemeta::core::JSON dividend{sourcemeta::core::Decimal{"1280.325"}};
const sourcemeta::core::JSON divisor{0.01};
EXPECT_FALSE(dividend.divisible_by(divisor));
}

TEST(JSON_decimal, divisible_by_decimal_real_0_1_true) {
const sourcemeta::core::JSON dividend{sourcemeta::core::Decimal{"100.3"}};
const sourcemeta::core::JSON divisor{0.1};
EXPECT_TRUE(dividend.divisible_by(divisor));
}

TEST(JSON_decimal, divisible_by_decimal_real_0_1_false) {
const sourcemeta::core::JSON dividend{sourcemeta::core::Decimal{"100.35"}};
const sourcemeta::core::JSON divisor{0.1};
EXPECT_FALSE(dividend.divisible_by(divisor));
}

TEST(JSON_decimal, divisible_by_decimal_real_0_001_true) {
const sourcemeta::core::JSON dividend{sourcemeta::core::Decimal{"25.123"}};
const sourcemeta::core::JSON divisor{0.001};
EXPECT_TRUE(dividend.divisible_by(divisor));
}

TEST(JSON_decimal, divisible_by_decimal_real_0_001_false) {
const sourcemeta::core::JSON dividend{sourcemeta::core::Decimal{"25.1235"}};
const sourcemeta::core::JSON divisor{0.001};
EXPECT_FALSE(dividend.divisible_by(divisor));
}

TEST(JSON_decimal, divisible_by_decimal_real_0_0001_true) {
const sourcemeta::core::JSON dividend{sourcemeta::core::Decimal{"99.9999"}};
const sourcemeta::core::JSON divisor{0.0001};
EXPECT_TRUE(dividend.divisible_by(divisor));
}

TEST(JSON_decimal, divisible_by_decimal_real_0_0001_false) {
const sourcemeta::core::JSON dividend{sourcemeta::core::Decimal{"99.99995"}};
const sourcemeta::core::JSON divisor{0.0001};
EXPECT_FALSE(dividend.divisible_by(divisor));
}

TEST(JSON_decimal, divisible_by_real_decimal_0_01_true) {
const sourcemeta::core::JSON dividend{1280.32};
const sourcemeta::core::JSON divisor{sourcemeta::core::Decimal{"0.01"}};
EXPECT_TRUE(dividend.divisible_by(divisor));
}

TEST(JSON_decimal, divisible_by_real_decimal_0_01_false) {
const sourcemeta::core::JSON dividend{1280.325};
const sourcemeta::core::JSON divisor{sourcemeta::core::Decimal{"0.01"}};
EXPECT_FALSE(dividend.divisible_by(divisor));
}

TEST(JSON_decimal, divisible_by_real_decimal_0_1_true) {
const sourcemeta::core::JSON dividend{100.3};
const sourcemeta::core::JSON divisor{sourcemeta::core::Decimal{"0.1"}};
EXPECT_TRUE(dividend.divisible_by(divisor));
}

TEST(JSON_decimal, divisible_by_real_decimal_0_1_false) {
const sourcemeta::core::JSON dividend{100.35};
const sourcemeta::core::JSON divisor{sourcemeta::core::Decimal{"0.1"}};
EXPECT_FALSE(dividend.divisible_by(divisor));
}

TEST(JSON_decimal, divisible_by_real_decimal_0_001_true) {
const sourcemeta::core::JSON dividend{25.123};
const sourcemeta::core::JSON divisor{sourcemeta::core::Decimal{"0.001"}};
EXPECT_TRUE(dividend.divisible_by(divisor));
}

TEST(JSON_decimal, divisible_by_real_decimal_0_001_false) {
const sourcemeta::core::JSON dividend{25.1235};
const sourcemeta::core::JSON divisor{sourcemeta::core::Decimal{"0.001"}};
EXPECT_FALSE(dividend.divisible_by(divisor));
}

TEST(JSON_decimal, divisible_by_real_decimal_0_0001_true) {
const sourcemeta::core::JSON dividend{99.9999};
const sourcemeta::core::JSON divisor{sourcemeta::core::Decimal{"0.0001"}};
EXPECT_TRUE(dividend.divisible_by(divisor));
}

TEST(JSON_decimal, divisible_by_real_decimal_0_0001_false) {
const sourcemeta::core::JSON dividend{99.99995};
const sourcemeta::core::JSON divisor{sourcemeta::core::Decimal{"0.0001"}};
EXPECT_FALSE(dividend.divisible_by(divisor));
}

TEST(JSON_decimal, divisible_by_integer_decimal_0_01_true) {
const sourcemeta::core::JSON dividend{100};
const sourcemeta::core::JSON divisor{sourcemeta::core::Decimal{"0.01"}};
EXPECT_TRUE(dividend.divisible_by(divisor));
}

TEST(JSON_decimal, divisible_by_integer_decimal_0_1_true) {
const sourcemeta::core::JSON dividend{10};
const sourcemeta::core::JSON divisor{sourcemeta::core::Decimal{"0.1"}};
EXPECT_TRUE(dividend.divisible_by(divisor));
}

TEST(JSON_decimal, divisible_by_integer_decimal_0_001_true) {
const sourcemeta::core::JSON dividend{1};
const sourcemeta::core::JSON divisor{sourcemeta::core::Decimal{"0.001"}};
EXPECT_TRUE(dividend.divisible_by(divisor));
}

TEST(JSON_decimal, divisible_by_integer_decimal_0_0001_true) {
const sourcemeta::core::JSON dividend{1};
const sourcemeta::core::JSON divisor{sourcemeta::core::Decimal{"0.0001"}};
EXPECT_TRUE(dividend.divisible_by(divisor));
}

TEST(JSON_decimal, divisible_by_negative_decimal_decimal_0_01_true) {
const sourcemeta::core::JSON dividend{sourcemeta::core::Decimal{"-1280.32"}};
const sourcemeta::core::JSON divisor{sourcemeta::core::Decimal{"0.01"}};
EXPECT_TRUE(dividend.divisible_by(divisor));
}

TEST(JSON_decimal, divisible_by_negative_decimal_real_0_01_true) {
const sourcemeta::core::JSON dividend{sourcemeta::core::Decimal{"-1280.32"}};
const sourcemeta::core::JSON divisor{0.01};
EXPECT_TRUE(dividend.divisible_by(divisor));
}

TEST(JSON_decimal, divisible_by_negative_real_decimal_0_01_true) {
const sourcemeta::core::JSON dividend{-1280.32};
const sourcemeta::core::JSON divisor{sourcemeta::core::Decimal{"0.01"}};
EXPECT_TRUE(dividend.divisible_by(divisor));
}

TEST(JSON_decimal, divisible_by_negative_integer_decimal_0_01_true) {
const sourcemeta::core::JSON dividend{-100};
const sourcemeta::core::JSON divisor{sourcemeta::core::Decimal{"0.01"}};
EXPECT_TRUE(dividend.divisible_by(divisor));
}

TEST(JSON_decimal, divisible_by_decimal_decimal_0_3_true) {
const sourcemeta::core::JSON dividend{sourcemeta::core::Decimal{"0.9"}};
const sourcemeta::core::JSON divisor{sourcemeta::core::Decimal{"0.3"}};
EXPECT_TRUE(dividend.divisible_by(divisor));
}

TEST(JSON_decimal, divisible_by_decimal_decimal_0_3_false) {
const sourcemeta::core::JSON dividend{sourcemeta::core::Decimal{"1.0"}};
const sourcemeta::core::JSON divisor{sourcemeta::core::Decimal{"0.3"}};
EXPECT_FALSE(dividend.divisible_by(divisor));
}

TEST(JSON_decimal, divisible_by_decimal_decimal_0_7_true) {
const sourcemeta::core::JSON dividend{sourcemeta::core::Decimal{"2.1"}};
const sourcemeta::core::JSON divisor{sourcemeta::core::Decimal{"0.7"}};
EXPECT_TRUE(dividend.divisible_by(divisor));
}

TEST(JSON_decimal, divisible_by_decimal_decimal_0_7_false) {
const sourcemeta::core::JSON dividend{sourcemeta::core::Decimal{"2.0"}};
const sourcemeta::core::JSON divisor{sourcemeta::core::Decimal{"0.7"}};
EXPECT_FALSE(dividend.divisible_by(divisor));
}

TEST(JSON_decimal, divisible_by_large_decimal_decimal_0_01_true) {
const sourcemeta::core::JSON dividend{sourcemeta::core::Decimal{"999999.99"}};
const sourcemeta::core::JSON divisor{sourcemeta::core::Decimal{"0.01"}};
EXPECT_TRUE(dividend.divisible_by(divisor));
}

TEST(JSON_decimal, divisible_by_large_decimal_real_0_01_true) {
const sourcemeta::core::JSON dividend{sourcemeta::core::Decimal{"999999.99"}};
const sourcemeta::core::JSON divisor{0.01};
EXPECT_TRUE(dividend.divisible_by(divisor));
}

TEST(JSON_decimal, divisible_by_large_decimal_decimal_0_001_true) {
const sourcemeta::core::JSON dividend{sourcemeta::core::Decimal{"12345.678"}};
const sourcemeta::core::JSON divisor{sourcemeta::core::Decimal{"0.001"}};
EXPECT_TRUE(dividend.divisible_by(divisor));
}

TEST(JSON_decimal, fast_hash_positive) {
const sourcemeta::core::JSON document{sourcemeta::core::Decimal{"3.14"}};
EXPECT_EQ(document.fast_hash(), 8);
Expand Down
Loading
Loading