/** * Copyright (C) 2018-present MongoDB, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the Server Side Public License, version 1, * as published by MongoDB, Inc. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * Server Side Public License for more details. * * You should have received a copy of the Server Side Public License * along with this program. If not, see * . * * As a special exception, the copyright holders give permission to link the * code of portions of this program with the OpenSSL library under certain * conditions as described in each individual source file and distribute * linked combinations including the program with the OpenSSL library. You * must comply with the Server Side Public License in all respects for * all of the code used other than as permitted herein. If you modify file(s) * with this exception, you may extend this exception to your version of the * file(s), but you are not obligated to do so. If you do not wish to do so, * delete this exception statement from your version. If you delete this * exception statement from all source files in the program, then also delete * it in the license file. */ #include "mongo/platform/basic.h" #include "mongo/db/pipeline/granularity_rounder.h" #include "mongo/db/exec/document_value/document.h" #include "mongo/db/exec/document_value/document_value_test_util.h" #include "mongo/db/pipeline/expression_context_for_test.h" #include "mongo/util/assert_util.h" namespace mongo { using boost::intrusive_ptr; using std::string; using std::vector; namespace { const double DELTA = 0.0001; const vector preferredNumberSeries{ "R5", "R10", "R20", "R40", "R80", "1-2-5", "E6", "E12", "E24", "E48", "E96", "E192"}; /** * Tests that two values 'actual' and 'expected' are equal. If one of the values is a double, then * we check to see if the values are approximately equal with tolerance 'delta'. */ void testEquals(Value actual, Value expected, double delta = DELTA) { if (actual.getType() == BSONType::NumberDouble || actual.getType() == BSONType::NumberDouble) { ASSERT_APPROX_EQUAL(actual.coerceToDouble(), expected.coerceToDouble(), delta); } else { ASSERT_VALUE_EQ(actual, expected); } } vector getSeries(intrusive_ptr rounder) { const auto* preferredNumbersRounder = dynamic_cast(rounder.get()); ASSERT(preferredNumbersRounder != nullptr); return preferredNumbersRounder->getSeries(); } /** * Gets the preferred number series from a GranularityRounder and converts all of the numbers to * Decimal128. This helps with testing that the GranularityRounders work with the Decimal128 * datatype. */ vector getSeriesDecimal(intrusive_ptr rounder) { const auto* preferredNumbersRounder = dynamic_cast(rounder.get()); ASSERT(preferredNumbersRounder != nullptr); auto doubleSeries = preferredNumbersRounder->getSeries(); vector decimalSeries; for (auto&& doubleNumber : doubleSeries) { decimalSeries.push_back(Decimal128(doubleNumber)); } return decimalSeries; } /** * Tests that a number in a preferred number series rounds up to the next number in the * series. For example, 1 is in the R5 series, so we test that it should round up to 1.6. */ void testRoundingUpInSeries(intrusive_ptr rounder) { auto series = getSeries(rounder); double multiplier = 0.00001; for (int j = 0; j < 6; j++) { for (size_t i = 1; i < series.size(); i++) { // Make sure that each element in the series at position i - 1 rounds up to the number // at position i. Value input = Value(series[i - 1] * multiplier); Value roundedValue = rounder->roundUp(input); Value expectedValue = Value(series[i] * multiplier); try { testEquals(roundedValue, expectedValue); } catch (...) { FAIL(str::stream() << "The GranularityRounder for " << rounder->getName() << " failed rounding up the value " << input.coerceToDouble() << " at multiplier level " << multiplier << ". Expected " << expectedValue.coerceToDouble() << ", but got " << roundedValue.coerceToDouble()); } } multiplier *= 10.0; } } /** * Tests that a number in a preferred number series rounds up to the next number in the * series when the numbers are Decimal128 values. */ void testRoundingUpInSeriesDecimal(intrusive_ptr rounder) { auto series = getSeriesDecimal(rounder); Decimal128 multiplier = Decimal128(0.00001); for (int j = 0; j < 6; j++) { for (size_t i = 1; i < series.size(); i++) { // Make sure that each element in the series at position i - 1 rounds up to the number // at position i. Value input = Value(series[i - 1].multiply(multiplier)); Value expectedValue = Value(series[i].multiply(multiplier)); Value roundedValue = rounder->roundUp(input); ASSERT_EQ(roundedValue.getType(), BSONType::NumberDecimal); try { testEquals(roundedValue, expectedValue); } catch (...) { FAIL(str::stream() << "The GranularityRounder for " << rounder->getName() << " failed rounding up the value " << input.coerceToDecimal().toString() << " at multiplier level " << multiplier.toString() << ". Expected " << expectedValue.coerceToDecimal().toString() << ", but got " << roundedValue.coerceToDecimal().toString()); } } multiplier = multiplier.multiply(Decimal128(10)); } } /** * Tests that a number in between two values in a preferred number series rounds up to the * appropriate value. For example, the values 1 and 1.6 are in the R5 series. We test that the value * 1.3 rounds up to 1.6. */ void testRoundingUpBetweenSeries(intrusive_ptr rounder) { auto series = getSeries(rounder); double multiplier = 0.00001; for (int j = 0; j < 6; j++) { for (size_t i = 1; i < series.size(); i++) { double lower = series[i - 1] * multiplier; double upper = series[i] * multiplier; double middle = (lower + upper) / 2.0; // Make sure a number in between two numbers in the series rounds up correctly. Value roundedValue = rounder->roundUp(Value(middle)); Value expectedValue = Value(upper); try { testEquals(roundedValue, expectedValue); } catch (...) { FAIL(str::stream() << "The GranularityRounder for " << rounder->getName() << " failed rounding up the value " << middle << " at multiplier level " << multiplier << ". Expected " << expectedValue.coerceToDouble() << ", but got " << roundedValue.coerceToDouble()); } } multiplier *= 10.0; } } /** * Tests that a number in between two values in a preferred number series rounds up to the * appropriate value when the values are Decimal128 values. */ void testRoundingUpBetweenSeriesDecimal(intrusive_ptr rounder) { auto series = getSeriesDecimal(rounder); Decimal128 multiplier = Decimal128(0.00001); for (int j = 0; j < 6; j++) { for (size_t i = 1; i < series.size(); i++) { Decimal128 lower = series[i - 1].multiply(multiplier); Decimal128 upper = series[i].multiply(multiplier); Decimal128 middle = (lower.add(upper)).divide(Decimal128(2)); // Make sure a number in between two numbers in the series rounds up correctly. Value expectedValue = Value(upper); Value roundedValue = rounder->roundUp(Value(middle)); ASSERT_EQ(roundedValue.getType(), BSONType::NumberDecimal); try { testEquals(roundedValue, expectedValue); } catch (...) { FAIL(str::stream() << "The GranularityRounder for " << rounder->getName() << " failed rounding up the value " << middle.toString() << " at multiplier level " << multiplier.toString() << ". Expected " << expectedValue.coerceToDecimal().toString() << ", but got " << roundedValue.coerceToDecimal().toString()); } } multiplier = multiplier.multiply(Decimal128(10)); } } /** * Tests that a number in a preferred number series rounds down to the previous number in the * series. For example, 1.6 is in the R5 series, so we test that it should round down to 1. */ void testRoundingDownInSeries(intrusive_ptr rounder) { auto series = getSeries(rounder); double multiplier = 0.00001; for (int j = 0; j < 6; j++) { // Make sure that each element in the series at position i rounds down to the number at // position i - 1. for (size_t i = series.size() - 1; i > 0; i--) { Value input = Value(series[i] * multiplier); Value roundedValue = rounder->roundDown(input); Value expectedValue = Value(series[i - 1] * multiplier); try { testEquals(roundedValue, expectedValue); } catch (...) { FAIL(str::stream() << "The GranularityRounder for " << rounder->getName() << " failed rounding down the value " << input.coerceToDouble() << " at multiplier level " << multiplier << ". Expected " << expectedValue.coerceToDouble() << ", but got " << roundedValue.coerceToDouble()); } } multiplier *= 10.0; } } /** * Tests that a number in a preferred number series rounds down to the previous number in the * series when the values are Decimal128 values. */ void testRoundingDownInSeriesDecimal(intrusive_ptr rounder) { auto series = getSeriesDecimal(rounder); Decimal128 multiplier = Decimal128(0.00001); for (int j = 0; j < 6; j++) { // Make sure that each element in the series at position i rounds down to the number at // position i - 1. for (size_t i = series.size() - 1; i > 0; i--) { Value input = Value(series[i].multiply(multiplier)); Value expectedValue = Value(series[i - 1].multiply(multiplier)); Value roundedValue = rounder->roundDown(input); ASSERT_EQ(roundedValue.getType(), BSONType::NumberDecimal); try { testEquals(roundedValue, expectedValue); } catch (...) { FAIL(str::stream() << "The GranularityRounder for " << rounder->getName() << " failed rounding down the value " << input.coerceToDecimal().toString() << " at multiplier level " << multiplier.toString() << ". Expected " << expectedValue.coerceToDecimal().toString() << ", but got " << roundedValue.coerceToDecimal().toString()); } } multiplier = multiplier.multiply(Decimal128(10)); } } /** * Tests that a number in between two values in a preferred number series rounds down to the * appropriate value. For example, the values 1 and 1.6 are in the R5 series. We test that the value * 1.3 rounds down to 1. */ void testRoundingDownBetweenSeries(intrusive_ptr rounder) { auto series = getSeries(rounder); double multiplier = 0.00001; for (int j = 0; j < 6; j++) { for (size_t i = 1; i < series.size(); i++) { double lower = series[i - 1] * multiplier; double upper = series[i] * multiplier; double middle = (lower + upper) / 2.0; // Make sure a number in between two numbers in the series rounds down correctly. Value roundedValue = rounder->roundDown(Value(middle)); Value expectedValue = Value(lower); try { testEquals(roundedValue, expectedValue); } catch (...) { FAIL(str::stream() << "The GranularityRounder for " << rounder->getName() << " failed rounding down the value " << middle << " at multiplier level " << multiplier << ". Expected " << expectedValue.coerceToDouble() << ", but got " << roundedValue.coerceToDouble()); } } multiplier *= 10.0; } } /** * Tests that a number in between two values in a preferred number series rounds down to the * appropriate value when the values are Decimal128 values. */ void testRoundingDownBetweenSeriesDecimal(intrusive_ptr rounder) { auto series = getSeriesDecimal(rounder); Decimal128 multiplier = Decimal128(0.00001); for (int j = 0; j < 6; j++) { for (size_t i = 1; i < series.size(); i++) { Decimal128 lower = series[i - 1].multiply(multiplier); Decimal128 upper = series[i].multiply(multiplier); Decimal128 middle = (lower.add(upper)).divide(Decimal128(2)); // Make sure a number in between two numbers in the series rounds down correctly. Value expectedValue = Value(lower); Value roundedValue = rounder->roundDown(Value(middle)); ASSERT_EQ(roundedValue.getType(), BSONType::NumberDecimal); try { testEquals(roundedValue, expectedValue); } catch (...) { FAIL(str::stream() << "The GranularityRounder for " << rounder->getName() << " failed rounding down the value " << middle.toString() << " at multiplier level " << multiplier.toString() << ". Expected " << expectedValue.coerceToDecimal().toString() << ", but got " << roundedValue.coerceToDecimal().toString()); } } multiplier = multiplier.multiply(Decimal128(10)); } } /** * Internally we represent preferred number series with a finite list of numbers. If we are rounding * a value outside the range of this list, we scale the values in the list by a power of 10. We * keep scaling until the number we are rounding falls into the range spanned by the preferred * numbers list. This method tests that the last value in the preferred numbers list rounds up to * the first value in the preferred numbers list multiplied by 10. This method also tests that the * first value in the preferred numbers list rounds down to the last value in the preferred numbers * list divided by 10. */ void testSeriesWrappingAround(intrusive_ptr rounder) { auto series = getSeries(rounder); double multiplier = 0.00001; for (int j = 0; j < 6; j++) { Value input = Value(series.back() * multiplier); Value roundedValue = rounder->roundUp(input); Value expectedValue = Value(series.front() * multiplier * 10.0); try { testEquals(roundedValue, expectedValue); } catch (...) { FAIL(str::stream() << "The GranularityRounder for " << rounder->getName() << " failed rounding up the value " << input.coerceToDouble() << " at multiplier level " << multiplier << ". Expected " << expectedValue.coerceToDouble() << ", but got " << roundedValue.coerceToDouble()); } input = Value(series.front() * multiplier); roundedValue = rounder->roundDown(input); expectedValue = Value(series.back() * multiplier / 10.0); try { testEquals(roundedValue, expectedValue); } catch (...) { FAIL(str::stream() << "The GranularityRounder for " << rounder->getName() << " failed rounding down the value " << input.coerceToDouble() << " at multiplier level " << multiplier << ". Expected " << expectedValue.coerceToDouble() << ", but got " << roundedValue.coerceToDouble()); } multiplier *= 10.0; } } /** * Tests that rounding wraps around when we are rounding Decimal128 values. */ void testSeriesWrappingAroundDecimal(intrusive_ptr rounder) { auto series = getSeriesDecimal(rounder); Decimal128 multiplier = Decimal128(0.00001); for (int j = 0; j < 6; j++) { Value input = Value(series.back().multiply(multiplier)); Value expectedValue = Value(series.front().multiply(multiplier).multiply(Decimal128(10))); Value roundedValue = rounder->roundUp(input); ASSERT_EQ(roundedValue.getType(), BSONType::NumberDecimal); try { testEquals(roundedValue, expectedValue); } catch (...) { FAIL(str::stream() << "The GranularityRounder for " << rounder->getName() << " failed rounding up the value " << input.coerceToDecimal().toString() << " at multiplier level " << multiplier.toString() << ". Expected " << expectedValue.coerceToDecimal().toString() << ", but got " << roundedValue.coerceToDecimal().toString()); } input = Value(series.front().multiply(multiplier)); expectedValue = Value(series.back().multiply(multiplier).divide(Decimal128(10))); roundedValue = rounder->roundDown(input); ASSERT_EQ(roundedValue.getType(), BSONType::NumberDecimal); try { testEquals(roundedValue, expectedValue); } catch (...) { FAIL(str::stream() << "The GranularityRounder for " << rounder->getName() << " failed rounding down the value " << input.coerceToDecimal().toString() << " at multiplier level " << multiplier.toString() << ". Expected " << expectedValue.coerceToDecimal().toString() << ", but got " << roundedValue.coerceToDecimal().toString()); } multiplier.multiply(Decimal128(10)); } } TEST(GranularityRounderPreferredNumbersTest, ShouldRoundUpNumberInSeriesToNextNumberInSeries) { for (auto&& series : preferredNumberSeries) { auto rounder = GranularityRounder::getGranularityRounder(new ExpressionContextForTest(), series); testRoundingUpInSeries(rounder); testRoundingUpInSeriesDecimal(rounder); } } TEST(GranularityRounderPreferredNumbersTest, ShouldRoundDownNumberInSeriesToPreviousNumberInSeries) { for (auto&& series : preferredNumberSeries) { auto rounder = GranularityRounder::getGranularityRounder(new ExpressionContextForTest(), series); testRoundingDownInSeries(rounder); testRoundingDownInSeriesDecimal(rounder); } } TEST(GranularityRounderPreferredNumbersTest, ShouldRoundUpValueInBetweenSeriesNumbers) { for (auto&& series : preferredNumberSeries) { auto rounder = GranularityRounder::getGranularityRounder(new ExpressionContextForTest(), series); testRoundingUpBetweenSeries(rounder); testRoundingUpBetweenSeriesDecimal(rounder); } } TEST(GranularityRounderPreferredNumbersTest, ShouldRoundDownValueInBetweenSeriesNumbers) { for (auto&& series : preferredNumberSeries) { auto rounder = GranularityRounder::getGranularityRounder(new ExpressionContextForTest(), series); testRoundingDownBetweenSeries(rounder); testRoundingDownBetweenSeriesDecimal(rounder); } } TEST(GranularityRounderPreferredNumbersTest, SeriesShouldWrapAroundWhenRounding) { for (auto&& series : preferredNumberSeries) { auto rounder = GranularityRounder::getGranularityRounder(new ExpressionContextForTest(), series); testSeriesWrappingAround(rounder); testSeriesWrappingAroundDecimal(rounder); } } TEST(GranularityRounderPreferredNumbersTest, ShouldRoundZeroToZero) { for (auto&& series : preferredNumberSeries) { auto rounder = GranularityRounder::getGranularityRounder(new ExpressionContextForTest(), series); // Make sure that each GranularityRounder rounds zero to zero. testEquals(rounder->roundUp(Value(0)), Value(0)); testEquals(rounder->roundDown(Value(0)), Value(0)); testEquals(rounder->roundUp(Value(Decimal128(0))), Value(Decimal128(0))); testEquals(rounder->roundDown(Value(Decimal128(0))), Value(Decimal128(0))); } } TEST(GranularityRounderPreferredNumbersTest, ShouldFailOnRoundingNonNumericValues) { for (auto&& series : preferredNumberSeries) { auto rounder = GranularityRounder::getGranularityRounder(new ExpressionContextForTest(), series); // Make sure that each GranularityRounder fails when rounding a non-numeric value. Value stringValue = Value("test"_sd); ASSERT_THROWS_CODE(rounder->roundUp(stringValue), AssertionException, 40262); ASSERT_THROWS_CODE(rounder->roundDown(stringValue), AssertionException, 40262); } } TEST(GranularityRounderPreferredNumbersTest, ShouldFailOnRoundingNaN) { for (auto&& series : preferredNumberSeries) { auto rounder = GranularityRounder::getGranularityRounder(new ExpressionContextForTest(), series); // Make sure that each GranularityRounder fails when rounding NaN. Value nan = Value(std::nan("NaN")); ASSERT_THROWS_CODE(rounder->roundUp(nan), AssertionException, 40263); ASSERT_THROWS_CODE(rounder->roundDown(nan), AssertionException, 40263); Value positiveNan = Value(Decimal128::kPositiveNaN); Value negativeNan = Value(Decimal128::kNegativeNaN); ASSERT_THROWS_CODE(rounder->roundUp(positiveNan), AssertionException, 40263); ASSERT_THROWS_CODE(rounder->roundDown(positiveNan), AssertionException, 40263); ASSERT_THROWS_CODE(rounder->roundUp(negativeNan), AssertionException, 40263); ASSERT_THROWS_CODE(rounder->roundDown(negativeNan), AssertionException, 40263); } } TEST(GranularityRounderPreferredNumbersTest, ShouldFailOnRoundingNegativeNumber) { for (auto&& series : preferredNumberSeries) { auto rounder = GranularityRounder::getGranularityRounder(new ExpressionContextForTest(), series); // Make sure that each GranularityRounder fails when rounding a negative number. Value negativeNumber = Value(-1); ASSERT_THROWS_CODE(rounder->roundUp(negativeNumber), AssertionException, 40268); ASSERT_THROWS_CODE(rounder->roundDown(negativeNumber), AssertionException, 40268); negativeNumber = Value(Decimal128(-1)); ASSERT_THROWS_CODE(rounder->roundUp(negativeNumber), AssertionException, 40268); ASSERT_THROWS_CODE(rounder->roundDown(negativeNumber), AssertionException, 40268); } } TEST(GranularityRounderPreferredNumbersTest, Infinity) { auto rounder = GranularityRounder::getGranularityRounder(new ExpressionContextForTest(), "E192"); Value inf = Value(std::numeric_limits::infinity()); ASSERT_TRUE(std::isinf(rounder->roundUp(inf).coerceToDouble())); ASSERT_TRUE(std::isinf(rounder->roundDown(inf).coerceToDouble())); Value decimalInf = Value(Decimal128::kPositiveInfinity); testEquals(rounder->roundUp(decimalInf), decimalInf); testEquals(rounder->roundDown(decimalInf), decimalInf); } } // namespace } // namespace mongo