From 35f747d35c415bf9d5959acfa4becf300303be86 Mon Sep 17 00:00:00 2001 From: jedel1043 Date: Tue, 24 Oct 2023 20:29:30 -0600 Subject: [PATCH 1/3] Introduce `RoundingIncrement` to `FixedDecimal` --- ffi/capi/c/include/ICU4XFixedDecimal.h | 3 + ffi/capi/c/include/ICU4XRoundingIncrement.h | 33 +++ .../cpp/docs/source/fixed_decimal_ffi.rst | 20 ++ ffi/capi/cpp/include/ICU4XFixedDecimal.h | 3 + ffi/capi/cpp/include/ICU4XFixedDecimal.hpp | 11 + ffi/capi/cpp/include/ICU4XRoundingIncrement.h | 33 +++ .../cpp/include/ICU4XRoundingIncrement.hpp | 28 +++ ffi/capi/js/docs/source/fixed_decimal_ffi.rst | 12 + ffi/capi/js/include/ICU4XFixedDecimal.d.ts | 7 + ffi/capi/js/include/ICU4XFixedDecimal.js | 5 + .../js/include/ICU4XRoundingIncrement.d.ts | 21 ++ ffi/capi/js/include/ICU4XRoundingIncrement.js | 23 ++ ffi/capi/js/include/index.d.ts | 1 + ffi/capi/js/include/index.js | 1 + ffi/capi/src/fixed_decimal.rs | 31 +++ utils/fixed_decimal/src/decimal.rs | 224 +++++++++++++++++- utils/fixed_decimal/src/lib.rs | 1 + 17 files changed, 445 insertions(+), 12 deletions(-) create mode 100644 ffi/capi/c/include/ICU4XRoundingIncrement.h create mode 100644 ffi/capi/cpp/include/ICU4XRoundingIncrement.h create mode 100644 ffi/capi/cpp/include/ICU4XRoundingIncrement.hpp create mode 100644 ffi/capi/js/include/ICU4XRoundingIncrement.d.ts create mode 100644 ffi/capi/js/include/ICU4XRoundingIncrement.js diff --git a/ffi/capi/c/include/ICU4XFixedDecimal.h b/ffi/capi/c/include/ICU4XFixedDecimal.h index 361294b6874..b1c7598466a 100644 --- a/ffi/capi/c/include/ICU4XFixedDecimal.h +++ b/ffi/capi/c/include/ICU4XFixedDecimal.h @@ -17,6 +17,7 @@ typedef struct ICU4XFixedDecimal ICU4XFixedDecimal; #include "diplomat_result_box_ICU4XFixedDecimal_ICU4XError.h" #include "ICU4XFixedDecimalSign.h" #include "ICU4XFixedDecimalSignDisplay.h" +#include "ICU4XRoundingIncrement.h" #include "diplomat_result_void_void.h" #ifdef __cplusplus namespace capi { @@ -73,6 +74,8 @@ void ICU4XFixedDecimal_set_max_position(ICU4XFixedDecimal* self, int16_t positio void ICU4XFixedDecimal_trunc(ICU4XFixedDecimal* self, int16_t position); +void ICU4XFixedDecimal_trunc_to_increment(ICU4XFixedDecimal* self, int16_t position, ICU4XRoundingIncrement increment); + void ICU4XFixedDecimal_half_trunc(ICU4XFixedDecimal* self, int16_t position); void ICU4XFixedDecimal_expand(ICU4XFixedDecimal* self, int16_t position); diff --git a/ffi/capi/c/include/ICU4XRoundingIncrement.h b/ffi/capi/c/include/ICU4XRoundingIncrement.h new file mode 100644 index 00000000000..c4b0666befa --- /dev/null +++ b/ffi/capi/c/include/ICU4XRoundingIncrement.h @@ -0,0 +1,33 @@ +#ifndef ICU4XRoundingIncrement_H +#define ICU4XRoundingIncrement_H +#include +#include +#include +#include +#include "diplomat_runtime.h" + +#ifdef __cplusplus +namespace capi { +#endif + +typedef enum ICU4XRoundingIncrement { + ICU4XRoundingIncrement_MultiplesOf1 = 0, + ICU4XRoundingIncrement_MultiplesOf2 = 1, + ICU4XRoundingIncrement_MultiplesOf5 = 2, + ICU4XRoundingIncrement_MultiplesOf25 = 3, +} ICU4XRoundingIncrement; +#ifdef __cplusplus +} // namespace capi +#endif +#ifdef __cplusplus +namespace capi { +extern "C" { +#endif + +void ICU4XRoundingIncrement_destroy(ICU4XRoundingIncrement* self); + +#ifdef __cplusplus +} // extern "C" +} // namespace capi +#endif +#endif diff --git a/ffi/capi/cpp/docs/source/fixed_decimal_ffi.rst b/ffi/capi/cpp/docs/source/fixed_decimal_ffi.rst index 6363a6501c7..51868e35d95 100644 --- a/ffi/capi/cpp/docs/source/fixed_decimal_ffi.rst +++ b/ffi/capi/cpp/docs/source/fixed_decimal_ffi.rst @@ -167,6 +167,11 @@ See the `Rust documentation for trunc `__ for more information. + .. cpp:function:: void trunc_to_increment(int16_t position, ICU4XRoundingIncrement increment) + + See the `Rust documentation for trunc_to_increment `__ for more information. + + .. cpp:function:: void half_trunc(int16_t position) See the `Rust documentation for half_trunc `__ for more information. @@ -270,3 +275,18 @@ .. cpp:enumerator:: ExceptZero .. cpp:enumerator:: Negative + +.. cpp:enum-struct:: ICU4XRoundingIncrement + + Increment used in a rounding operation. + + See the `Rust documentation for RoundingIncrement `__ for more information. + + + .. cpp:enumerator:: MultiplesOf1 + + .. cpp:enumerator:: MultiplesOf2 + + .. cpp:enumerator:: MultiplesOf5 + + .. cpp:enumerator:: MultiplesOf25 diff --git a/ffi/capi/cpp/include/ICU4XFixedDecimal.h b/ffi/capi/cpp/include/ICU4XFixedDecimal.h index 361294b6874..b1c7598466a 100644 --- a/ffi/capi/cpp/include/ICU4XFixedDecimal.h +++ b/ffi/capi/cpp/include/ICU4XFixedDecimal.h @@ -17,6 +17,7 @@ typedef struct ICU4XFixedDecimal ICU4XFixedDecimal; #include "diplomat_result_box_ICU4XFixedDecimal_ICU4XError.h" #include "ICU4XFixedDecimalSign.h" #include "ICU4XFixedDecimalSignDisplay.h" +#include "ICU4XRoundingIncrement.h" #include "diplomat_result_void_void.h" #ifdef __cplusplus namespace capi { @@ -73,6 +74,8 @@ void ICU4XFixedDecimal_set_max_position(ICU4XFixedDecimal* self, int16_t positio void ICU4XFixedDecimal_trunc(ICU4XFixedDecimal* self, int16_t position); +void ICU4XFixedDecimal_trunc_to_increment(ICU4XFixedDecimal* self, int16_t position, ICU4XRoundingIncrement increment); + void ICU4XFixedDecimal_half_trunc(ICU4XFixedDecimal* self, int16_t position); void ICU4XFixedDecimal_expand(ICU4XFixedDecimal* self, int16_t position); diff --git a/ffi/capi/cpp/include/ICU4XFixedDecimal.hpp b/ffi/capi/cpp/include/ICU4XFixedDecimal.hpp index 5398d7af0a8..0ca931b7a6c 100644 --- a/ffi/capi/cpp/include/ICU4XFixedDecimal.hpp +++ b/ffi/capi/cpp/include/ICU4XFixedDecimal.hpp @@ -15,6 +15,7 @@ class ICU4XFixedDecimal; #include "ICU4XError.hpp" #include "ICU4XFixedDecimalSign.hpp" #include "ICU4XFixedDecimalSignDisplay.hpp" +#include "ICU4XRoundingIncrement.hpp" /** * A destruction policy for using ICU4XFixedDecimal with std::unique_ptr. @@ -218,6 +219,13 @@ class ICU4XFixedDecimal { */ void trunc(int16_t position); + /** + * + * + * See the [Rust documentation for `trunc_to_increment`](https://docs.rs/fixed_decimal/latest/fixed_decimal/struct.FixedDecimal.html#method.trunc_to_increment) for more information. + */ + void trunc_to_increment(int16_t position, ICU4XRoundingIncrement increment); + /** * * @@ -419,6 +427,9 @@ inline void ICU4XFixedDecimal::set_max_position(int16_t position) { inline void ICU4XFixedDecimal::trunc(int16_t position) { capi::ICU4XFixedDecimal_trunc(this->inner.get(), position); } +inline void ICU4XFixedDecimal::trunc_to_increment(int16_t position, ICU4XRoundingIncrement increment) { + capi::ICU4XFixedDecimal_trunc_to_increment(this->inner.get(), position, static_cast(increment)); +} inline void ICU4XFixedDecimal::half_trunc(int16_t position) { capi::ICU4XFixedDecimal_half_trunc(this->inner.get(), position); } diff --git a/ffi/capi/cpp/include/ICU4XRoundingIncrement.h b/ffi/capi/cpp/include/ICU4XRoundingIncrement.h new file mode 100644 index 00000000000..c4b0666befa --- /dev/null +++ b/ffi/capi/cpp/include/ICU4XRoundingIncrement.h @@ -0,0 +1,33 @@ +#ifndef ICU4XRoundingIncrement_H +#define ICU4XRoundingIncrement_H +#include +#include +#include +#include +#include "diplomat_runtime.h" + +#ifdef __cplusplus +namespace capi { +#endif + +typedef enum ICU4XRoundingIncrement { + ICU4XRoundingIncrement_MultiplesOf1 = 0, + ICU4XRoundingIncrement_MultiplesOf2 = 1, + ICU4XRoundingIncrement_MultiplesOf5 = 2, + ICU4XRoundingIncrement_MultiplesOf25 = 3, +} ICU4XRoundingIncrement; +#ifdef __cplusplus +} // namespace capi +#endif +#ifdef __cplusplus +namespace capi { +extern "C" { +#endif + +void ICU4XRoundingIncrement_destroy(ICU4XRoundingIncrement* self); + +#ifdef __cplusplus +} // extern "C" +} // namespace capi +#endif +#endif diff --git a/ffi/capi/cpp/include/ICU4XRoundingIncrement.hpp b/ffi/capi/cpp/include/ICU4XRoundingIncrement.hpp new file mode 100644 index 00000000000..be93aa0401c --- /dev/null +++ b/ffi/capi/cpp/include/ICU4XRoundingIncrement.hpp @@ -0,0 +1,28 @@ +#ifndef ICU4XRoundingIncrement_HPP +#define ICU4XRoundingIncrement_HPP +#include +#include +#include +#include +#include +#include +#include +#include "diplomat_runtime.hpp" + +#include "ICU4XRoundingIncrement.h" + + + +/** + * Increment used in a rounding operation. + * + * See the [Rust documentation for `RoundingIncrement`](https://docs.rs/fixed_decimal/latest/fixed_decimal/enum.RoundingIncrement.html) for more information. + */ +enum struct ICU4XRoundingIncrement { + MultiplesOf1 = 0, + MultiplesOf2 = 1, + MultiplesOf5 = 2, + MultiplesOf25 = 3, +}; + +#endif diff --git a/ffi/capi/js/docs/source/fixed_decimal_ffi.rst b/ffi/capi/js/docs/source/fixed_decimal_ffi.rst index ed489730a56..dba51a77161 100644 --- a/ffi/capi/js/docs/source/fixed_decimal_ffi.rst +++ b/ffi/capi/js/docs/source/fixed_decimal_ffi.rst @@ -167,6 +167,11 @@ See the `Rust documentation for trunc `__ for more information. + .. js:method:: trunc_to_increment(position, increment) + + See the `Rust documentation for trunc_to_increment `__ for more information. + + .. js:method:: half_trunc(position) See the `Rust documentation for half_trunc `__ for more information. @@ -238,3 +243,10 @@ See the `Rust documentation for SignDisplay `__ for more information. + +.. js:class:: ICU4XRoundingIncrement + + Increment used in a rounding operation. + + See the `Rust documentation for RoundingIncrement `__ for more information. + diff --git a/ffi/capi/js/include/ICU4XFixedDecimal.d.ts b/ffi/capi/js/include/ICU4XFixedDecimal.d.ts index dfa978cbfd4..6a41614e9e9 100644 --- a/ffi/capi/js/include/ICU4XFixedDecimal.d.ts +++ b/ffi/capi/js/include/ICU4XFixedDecimal.d.ts @@ -3,6 +3,7 @@ import { FFIError } from "./diplomat-runtime" import { ICU4XError } from "./ICU4XError"; import { ICU4XFixedDecimalSign } from "./ICU4XFixedDecimalSign"; import { ICU4XFixedDecimalSignDisplay } from "./ICU4XFixedDecimalSignDisplay"; +import { ICU4XRoundingIncrement } from "./ICU4XRoundingIncrement"; /** @@ -201,6 +202,12 @@ export class ICU4XFixedDecimal { */ trunc(position: i16): void; + /** + + * See the {@link https://docs.rs/fixed_decimal/latest/fixed_decimal/struct.FixedDecimal.html#method.trunc_to_increment Rust documentation for `trunc_to_increment`} for more information. + */ + trunc_to_increment(position: i16, increment: ICU4XRoundingIncrement): void; + /** * See the {@link https://docs.rs/fixed_decimal/latest/fixed_decimal/struct.FixedDecimal.html#method.half_trunc Rust documentation for `half_trunc`} for more information. diff --git a/ffi/capi/js/include/ICU4XFixedDecimal.js b/ffi/capi/js/include/ICU4XFixedDecimal.js index e517bc9f48c..f2733123d32 100644 --- a/ffi/capi/js/include/ICU4XFixedDecimal.js +++ b/ffi/capi/js/include/ICU4XFixedDecimal.js @@ -3,6 +3,7 @@ import * as diplomatRuntime from "./diplomat-runtime.js" import { ICU4XError_js_to_rust, ICU4XError_rust_to_js } from "./ICU4XError.js" import { ICU4XFixedDecimalSign_js_to_rust, ICU4XFixedDecimalSign_rust_to_js } from "./ICU4XFixedDecimalSign.js" import { ICU4XFixedDecimalSignDisplay_js_to_rust, ICU4XFixedDecimalSignDisplay_rust_to_js } from "./ICU4XFixedDecimalSignDisplay.js" +import { ICU4XRoundingIncrement_js_to_rust, ICU4XRoundingIncrement_rust_to_js } from "./ICU4XRoundingIncrement.js" const ICU4XFixedDecimal_box_destroy_registry = new FinalizationRegistry(underlying => { wasm.ICU4XFixedDecimal_destroy(underlying); @@ -186,6 +187,10 @@ export class ICU4XFixedDecimal { wasm.ICU4XFixedDecimal_trunc(this.underlying, arg_position); } + trunc_to_increment(arg_position, arg_increment) { + wasm.ICU4XFixedDecimal_trunc_to_increment(this.underlying, arg_position, ICU4XRoundingIncrement_js_to_rust[arg_increment]); + } + half_trunc(arg_position) { wasm.ICU4XFixedDecimal_half_trunc(this.underlying, arg_position); } diff --git a/ffi/capi/js/include/ICU4XRoundingIncrement.d.ts b/ffi/capi/js/include/ICU4XRoundingIncrement.d.ts new file mode 100644 index 00000000000..5d3c6763184 --- /dev/null +++ b/ffi/capi/js/include/ICU4XRoundingIncrement.d.ts @@ -0,0 +1,21 @@ + +/** + + * Increment used in a rounding operation. + + * See the {@link https://docs.rs/fixed_decimal/latest/fixed_decimal/enum.RoundingIncrement.html Rust documentation for `RoundingIncrement`} for more information. + */ +export enum ICU4XRoundingIncrement { + /** + */ + MultiplesOf1 = 'MultiplesOf1', + /** + */ + MultiplesOf2 = 'MultiplesOf2', + /** + */ + MultiplesOf5 = 'MultiplesOf5', + /** + */ + MultiplesOf25 = 'MultiplesOf25', +} \ No newline at end of file diff --git a/ffi/capi/js/include/ICU4XRoundingIncrement.js b/ffi/capi/js/include/ICU4XRoundingIncrement.js new file mode 100644 index 00000000000..c93a450e286 --- /dev/null +++ b/ffi/capi/js/include/ICU4XRoundingIncrement.js @@ -0,0 +1,23 @@ +import wasm from "./diplomat-wasm.mjs" +import * as diplomatRuntime from "./diplomat-runtime.js" + +export const ICU4XRoundingIncrement_js_to_rust = { + "MultiplesOf1": 0, + "MultiplesOf2": 1, + "MultiplesOf5": 2, + "MultiplesOf25": 3, +}; + +export const ICU4XRoundingIncrement_rust_to_js = { + [0]: "MultiplesOf1", + [1]: "MultiplesOf2", + [2]: "MultiplesOf5", + [3]: "MultiplesOf25", +}; + +export const ICU4XRoundingIncrement = { + "MultiplesOf1": "MultiplesOf1", + "MultiplesOf2": "MultiplesOf2", + "MultiplesOf5": "MultiplesOf5", + "MultiplesOf25": "MultiplesOf25", +}; diff --git a/ffi/capi/js/include/index.d.ts b/ffi/capi/js/include/index.d.ts index 9a4911392aa..ef7b2e1cc2c 100644 --- a/ffi/capi/js/include/index.d.ts +++ b/ffi/capi/js/include/index.d.ts @@ -97,6 +97,7 @@ export { ICU4XPluralRulesWithRanges } from './ICU4XPluralRulesWithRanges.js'; export { ICU4XPropertyValueNameToEnumMapper } from './ICU4XPropertyValueNameToEnumMapper.js'; export { ICU4XRegionDisplayNames } from './ICU4XRegionDisplayNames.js'; export { ICU4XReorderedIndexMap } from './ICU4XReorderedIndexMap.js'; +export { ICU4XRoundingIncrement } from './ICU4XRoundingIncrement.js'; export { ICU4XScriptExtensionsSet } from './ICU4XScriptExtensionsSet.js'; export { ICU4XScriptWithExtensions } from './ICU4XScriptWithExtensions.js'; export { ICU4XScriptWithExtensionsBorrowed } from './ICU4XScriptWithExtensionsBorrowed.js'; diff --git a/ffi/capi/js/include/index.js b/ffi/capi/js/include/index.js index e438b2f787c..6a7a17d32bf 100644 --- a/ffi/capi/js/include/index.js +++ b/ffi/capi/js/include/index.js @@ -97,6 +97,7 @@ export { ICU4XPluralRulesWithRanges } from './ICU4XPluralRulesWithRanges.js'; export { ICU4XPropertyValueNameToEnumMapper } from './ICU4XPropertyValueNameToEnumMapper.js'; export { ICU4XRegionDisplayNames } from './ICU4XRegionDisplayNames.js'; export { ICU4XReorderedIndexMap } from './ICU4XReorderedIndexMap.js'; +export { ICU4XRoundingIncrement } from './ICU4XRoundingIncrement.js'; export { ICU4XScriptExtensionsSet } from './ICU4XScriptExtensionsSet.js'; export { ICU4XScriptWithExtensions } from './ICU4XScriptWithExtensions.js'; export { ICU4XScriptWithExtensionsBorrowed } from './ICU4XScriptWithExtensionsBorrowed.js'; diff --git a/ffi/capi/src/fixed_decimal.rs b/ffi/capi/src/fixed_decimal.rs index b30f0dc59ea..7e5ffb5f914 100644 --- a/ffi/capi/src/fixed_decimal.rs +++ b/ffi/capi/src/fixed_decimal.rs @@ -2,6 +2,7 @@ // called LICENSE at the top level of the ICU4X source tree // (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). +use fixed_decimal::RoundingIncrement; use fixed_decimal::Sign; use fixed_decimal::SignDisplay; @@ -38,6 +39,15 @@ pub mod ffi { Negative, } + /// Increment used in a rounding operation. + #[diplomat::rust_link(fixed_decimal::RoundingIncrement, Enum)] + pub enum ICU4XRoundingIncrement { + MultiplesOf1, + MultiplesOf2, + MultiplesOf5, + MultiplesOf25, + } + impl ICU4XFixedDecimal { /// Construct an [`ICU4XFixedDecimal`] from an integer. #[diplomat::rust_link(fixed_decimal::FixedDecimal, Struct)] @@ -220,6 +230,16 @@ pub mod ffi { self.0.trunc(position) } + #[diplomat::rust_link(fixed_decimal::FixedDecimal::trunc_to_increment, FnInStruct)] + #[diplomat::rust_link( + fixed_decimal::FixedDecimal::trunced_to_increment, + FnInStruct, + hidden + )] + pub fn trunc_to_increment(&mut self, position: i16, increment: ICU4XRoundingIncrement) { + self.0.trunc_to_increment(position, increment.into()) + } + #[diplomat::rust_link(fixed_decimal::FixedDecimal::half_trunc, FnInStruct)] #[diplomat::rust_link(fixed_decimal::FixedDecimal::half_trunced, FnInStruct, hidden)] pub fn half_trunc(&mut self, position: i16) { @@ -321,3 +341,14 @@ impl From for SignDisplay { } } } + +impl From for RoundingIncrement { + fn from(value: ffi::ICU4XRoundingIncrement) -> Self { + match value { + ffi::ICU4XRoundingIncrement::MultiplesOf1 => RoundingIncrement::MultiplesOf1, + ffi::ICU4XRoundingIncrement::MultiplesOf2 => RoundingIncrement::MultiplesOf2, + ffi::ICU4XRoundingIncrement::MultiplesOf5 => RoundingIncrement::MultiplesOf5, + ffi::ICU4XRoundingIncrement::MultiplesOf25 => RoundingIncrement::MultiplesOf25, + } + } +} diff --git a/utils/fixed_decimal/src/decimal.rs b/utils/fixed_decimal/src/decimal.rs index 363d57d2308..c2e88742704 100644 --- a/utils/fixed_decimal/src/decimal.rs +++ b/utils/fixed_decimal/src/decimal.rs @@ -159,6 +159,26 @@ pub enum SignDisplay { Negative, } +/// Increment used in a rounding operation. +/// +/// Forces a rounding operation to round to only multiples of the specified increment. +#[derive(Debug, Eq, PartialEq, Clone, Copy, Default)] +#[non_exhaustive] +pub enum RoundingIncrement { + /// Round the last digit to any digit (0-9). + /// + /// This is the default rounding increment for all the methods that don't take a + /// `RoundingIncrement` as an argument. + #[default] + MultiplesOf1, + /// Round the last digit to only multiples of two (0, 2, 4, 6, 8). + MultiplesOf2, + /// Round the last digit to only multiples of five (0, 5). + MultiplesOf5, + /// Round the last two digits to only multiples of twenty-five (0, 25, 50, 75). + MultiplesOf25, +} + impl Default for FixedDecimal { /// Returns a `FixedDecimal` representing zero. fn default() -> Self { @@ -989,6 +1009,60 @@ impl FixedDecimal { /// assert_eq!("1", dec.to_string()); /// ``` pub fn trunc(&mut self, position: i16) { + self.trunc_to_increment(position, RoundingIncrement::MultiplesOf1) + } + + /// Truncates the number on the right to a particular position and rounding + /// increment, deleting digits if necessary. + /// + /// # Examples + /// + /// ``` + /// use fixed_decimal::{FixedDecimal, RoundingIncrement}; + /// # use std::str::FromStr; + /// + /// let mut dec = FixedDecimal::from_str("-3.5").unwrap(); + /// assert_eq!("-2", dec.trunced_to_increment(0, RoundingIncrement::MultiplesOf2).to_string()); + /// let mut dec = FixedDecimal::from_str("7.57").unwrap(); + /// assert_eq!("7.5", dec.trunced_to_increment(-1, RoundingIncrement::MultiplesOf5).to_string()); + /// let mut dec = FixedDecimal::from_str("5.45").unwrap(); + /// assert_eq!("5.25", dec.trunced_to_increment(-2, RoundingIncrement::MultiplesOf25).to_string()); + /// let mut dec = FixedDecimal::from_str("9.99").unwrap(); + /// assert_eq!("7.5", dec.trunced_to_increment(-1, RoundingIncrement::MultiplesOf25).to_string()); + /// let mut dec = FixedDecimal::from_str("9.99").unwrap(); + /// assert_eq!("9.98", dec.trunced_to_increment(-2, RoundingIncrement::MultiplesOf2).to_string()); + /// ``` + pub fn trunced_to_increment(mut self, position: i16, increment: RoundingIncrement) -> Self { + self.trunc_to_increment(position, increment); + self + } + + /// Truncates the number on the right to a particular position and rounding + /// increment, deleting digits if necessary. + /// + /// # Examples + /// + /// ``` + /// use fixed_decimal::{FixedDecimal, RoundingIncrement}; + /// # use std::str::FromStr; + /// + /// let mut dec = FixedDecimal::from_str("-3.5").unwrap(); + /// dec.trunc_to_increment(0, RoundingIncrement::MultiplesOf2); + /// assert_eq!("-2", dec.to_string()); + /// let mut dec = FixedDecimal::from_str("7.57").unwrap(); + /// dec.trunc_to_increment(-1, RoundingIncrement::MultiplesOf5); + /// assert_eq!("7.5", dec.to_string()); + /// let mut dec = FixedDecimal::from_str("5.45").unwrap(); + /// dec.trunc_to_increment(-2, RoundingIncrement::MultiplesOf25); + /// assert_eq!("5.25", dec.to_string()); + /// let mut dec = FixedDecimal::from_str("9.99").unwrap(); + /// dec.trunc_to_increment(-1, RoundingIncrement::MultiplesOf25); + /// assert_eq!("7.5", dec.to_string()); + /// let mut dec = FixedDecimal::from_str("9.99").unwrap(); + /// dec.trunc_to_increment(-2, RoundingIncrement::MultiplesOf2); + /// assert_eq!("9.98", dec.to_string()); + /// ``` + pub fn trunc_to_increment(&mut self, position: i16, increment: RoundingIncrement) { // 1. Set upper and lower magnitude self.lower_magnitude = cmp::min(position, 0); if position == i16::MIN { @@ -1000,8 +1074,21 @@ impl FixedDecimal { let magnitude = position - 1; self.upper_magnitude = cmp::max(self.upper_magnitude, magnitude); - // 2. If the rounding position is *lower than* the rightmost nonzero digit, exit early - if self.is_zero() || magnitude < self.nonzero_magnitude_end() { + // 2. If the rounding position is *lower than* the required digits to round to the next + // multiple, exit early. + let past_required_digits = match increment { + RoundingIncrement::MultiplesOf1 => magnitude < self.nonzero_magnitude_end(), + RoundingIncrement::MultiplesOf2 | RoundingIncrement::MultiplesOf5 => { + // We can early exit if the digit at `magnitude + 1` is zero. + magnitude + 1 < self.nonzero_magnitude_end() + } + RoundingIncrement::MultiplesOf25 => { + // We can early exit if the digits at `magniture + 1` and `magnitude + 2` are + // both zeroes. + magnitude + 2 < self.nonzero_magnitude_end() + } + }; + if self.is_zero() || past_required_digits { #[cfg(debug_assertions)] self.check_invariants(); return; @@ -1010,22 +1097,81 @@ impl FixedDecimal { // 3. If the rounding position is *in the middle* of the nonzero digits if magnitude < self.magnitude { // 3a. Calculate the number of digits to retain and remove the rest - let digits_to_retain = crate::ops::i16_abs_sub(self.magnitude, magnitude); - self.digits.truncate(digits_to_retain as usize); - // 3b. Remove trailing zeros from self.digits to retain invariants - // Note: this does not affect visible trailing zeros, - // which is tracked by self.lower_magnitude - let position_past_last_nonzero_digit = self + let digits_to_retain = crate::ops::i16_abs_sub(self.magnitude, magnitude) as usize; + self.digits.truncate(digits_to_retain); + + // 3b. Truncate to the previous multiple. + match increment { + RoundingIncrement::MultiplesOf1 => { + // No need to do more work, trailing zeroes are removed below. + } + RoundingIncrement::MultiplesOf2 => { + let Some(last_digit) = self.digits.last_mut() else { + // Unreachable + return; + }; + + // Equivalent to (n / 2) * 2, which truncates to the previous + // multiple of two + *last_digit &= 0xFE; + } + RoundingIncrement::MultiplesOf5 => { + let Some(last_digit) = self.digits.last_mut() else { + // Unreachable + return; + }; + + *last_digit = if *last_digit < 5 { 0 } else { 5 }; + } + RoundingIncrement::MultiplesOf25 => { + // Extend with zeroes to have the correct trailing digits. + self.digits.resize(digits_to_retain, 0); + + let Some((last_digit, digits)) = self.digits.split_last_mut() else { + // Unreachable. + return; + }; + + if let Some(second_last_digit) = digits.last_mut() { + let number = *second_last_digit * 10 + *last_digit; + + // Trailing zeroes will be removed below. We can defer + // the deletion to there. + (*second_last_digit, *last_digit) = if number < 25 { + (0, 0) + } else if number < 50 { + (2, 5) + } else if number < 75 { + (5, 0) + } else { + (7, 5) + }; + } else { + // The number has no other digits aside from the last, + // making it strictly less than 25. + *last_digit = 0; + }; + } + } + + // 3c. Handle the case where `digits` has trailing zeroes after + // truncating to the previous multiple. + let position_last_nonzero_digit = self .digits .iter() .rposition(|x| *x != 0) .map(|x| x + 1) .unwrap_or(0); - self.digits.truncate(position_past_last_nonzero_digit); - // 3c. By the invariant, there should still be at least 1 nonzero digit - debug_assert!(!self.digits.is_empty()); + self.digits.truncate(position_last_nonzero_digit); + + // 3d. If `digits` had only trailing zeroes after truncating, + // reset to zero. + if self.digits.is_empty() { + self.magnitude = 0; + } } else { - // 4. If the rounding position is *above* the leftmost nonzero digit, set to zero + // 4. If the rounding position is *above* the leftmost nonzero + // digit, set to zero self.digits.clear(); self.magnitude = 0; } @@ -3463,3 +3609,57 @@ fn test_concatenate() { } } } + +#[test] +fn test_rounding_increment() { + // Test Truncate Right + let mut dec = FixedDecimal::from(4235970).multiplied_pow10(-3); + assert_eq!("4235.970", dec.to_string()); + + dec.trunc_to_increment(-2, RoundingIncrement::MultiplesOf2); + assert_eq!("4235.96", dec.to_string()); + + dec.trunc_to_increment(-1, RoundingIncrement::MultiplesOf5); + assert_eq!("4235.5", dec.to_string()); + + dec.trunc_to_increment(0, RoundingIncrement::MultiplesOf25); + assert_eq!("4225", dec.to_string()); + + dec.trunc_to_increment(5, RoundingIncrement::MultiplesOf5); + assert_eq!("00000", dec.to_string()); + + dec.trunc_to_increment(2, RoundingIncrement::MultiplesOf2); + assert_eq!("00000", dec.to_string()); + + let mut dec = FixedDecimal::from_str("-99.999").unwrap(); + dec.trunc_to_increment(-2, RoundingIncrement::MultiplesOf25); + assert_eq!("-99.75", dec.to_string()); + + let mut dec = FixedDecimal::from_str("1234.56").unwrap(); + dec.trunc_to_increment(-1, RoundingIncrement::MultiplesOf2); + assert_eq!("1234.4", dec.to_string()); + + let mut dec = FixedDecimal::from_str("0.009").unwrap(); + dec.trunc_to_increment(-1, RoundingIncrement::MultiplesOf5); + assert_eq!("0.0", dec.to_string()); + + let mut dec = FixedDecimal::from_str("0.60").unwrap(); + dec.trunc_to_increment(-2, RoundingIncrement::MultiplesOf25); + assert_eq!("0.50", dec.to_string()); + + let mut dec = FixedDecimal::from_str("0.40").unwrap(); + dec.trunc_to_increment(-2, RoundingIncrement::MultiplesOf25); + assert_eq!("0.25", dec.to_string()); + + let mut dec = FixedDecimal::from_str("0.7000000099").unwrap(); + dec.trunc_to_increment(-3, RoundingIncrement::MultiplesOf2); + assert_eq!("0.700", dec.to_string()); + + let mut dec = FixedDecimal::from_str("5").unwrap(); + dec.trunc_to_increment(0, RoundingIncrement::MultiplesOf25); + assert_eq!("0", dec.to_string()); + + let mut dec = FixedDecimal::from_str("0.3").unwrap(); + dec.trunc_to_increment(-1, RoundingIncrement::MultiplesOf2); + assert_eq!("0.2", dec.to_string()); +} diff --git a/utils/fixed_decimal/src/lib.rs b/utils/fixed_decimal/src/lib.rs index 94029268a38..39c863ed629 100644 --- a/utils/fixed_decimal/src/lib.rs +++ b/utils/fixed_decimal/src/lib.rs @@ -67,6 +67,7 @@ pub use FloatPrecision as DoublePrecision; pub use compact::CompactDecimal; pub use decimal::FixedDecimal; +pub use decimal::RoundingIncrement; pub use decimal::Sign; pub use decimal::SignDisplay; use displaydoc::Display; From 08032c4b099e90bc5a056cf6a5461b15361c00be Mon Sep 17 00:00:00 2001 From: jedel1043 Date: Thu, 26 Oct 2023 23:03:36 -0600 Subject: [PATCH 2/3] Correctly handle `i16::MIN` and `i16::MAX` --- utils/fixed_decimal/src/decimal.rs | 81 ++++++++++++++++++++++-------- 1 file changed, 61 insertions(+), 20 deletions(-) diff --git a/utils/fixed_decimal/src/decimal.rs b/utils/fixed_decimal/src/decimal.rs index c2e88742704..d7366c09cb8 100644 --- a/utils/fixed_decimal/src/decimal.rs +++ b/utils/fixed_decimal/src/decimal.rs @@ -1065,29 +1065,50 @@ impl FixedDecimal { pub fn trunc_to_increment(&mut self, position: i16, increment: RoundingIncrement) { // 1. Set upper and lower magnitude self.lower_magnitude = cmp::min(position, 0); - if position == i16::MIN { - // Nothing more to do - #[cfg(debug_assertions)] - self.check_invariants(); - return; + + match position.cmp(&i16::MIN) { + // Don't return if the increment is not one, because we + // also need to round to the next increment if necessary. + Ordering::Equal if increment == RoundingIncrement::MultiplesOf1 => { + // Nothing more to do + #[cfg(debug_assertions)] + self.check_invariants(); + return; + } + Ordering::Greater => { + self.upper_magnitude = cmp::max(self.upper_magnitude, position - 1); + } + _ => { + // Ordering::Less is unreachable, and Ordering::Equal needs to apply roundings + // when the increment is not equal to 1. + // We don't override `self.upper_magnitude` because `self.upper_magnitude` is + // always equal or bigger than `i16::MIN`. + } } - let magnitude = position - 1; - self.upper_magnitude = cmp::max(self.upper_magnitude, magnitude); // 2. If the rounding position is *lower than* the required digits to round to the next // multiple, exit early. + // Fallbacks to `false` if `position == i16::MAX`. let past_required_digits = match increment { - RoundingIncrement::MultiplesOf1 => magnitude < self.nonzero_magnitude_end(), + RoundingIncrement::MultiplesOf1 => { + // We can early exit if `position` is the last digit + position <= self.nonzero_magnitude_end() + } RoundingIncrement::MultiplesOf2 | RoundingIncrement::MultiplesOf5 => { - // We can early exit if the digit at `magnitude + 1` is zero. - magnitude + 1 < self.nonzero_magnitude_end() + // We can early exit if the digit at `position` is zero. + position < self.nonzero_magnitude_end() } RoundingIncrement::MultiplesOf25 => { - // We can early exit if the digits at `magniture + 1` and `magnitude + 2` are - // both zeroes. - magnitude + 2 < self.nonzero_magnitude_end() + if position != i16::MAX { + // We can early exit if the digits at `position` and `position + 1` are + // both zeroes. + position + 1 < self.nonzero_magnitude_end() + } else { + false + } } }; + if self.is_zero() || past_required_digits { #[cfg(debug_assertions)] self.check_invariants(); @@ -1095,10 +1116,10 @@ impl FixedDecimal { } // 3. If the rounding position is *in the middle* of the nonzero digits - if magnitude < self.magnitude { + if position <= self.magnitude { // 3a. Calculate the number of digits to retain and remove the rest - let digits_to_retain = crate::ops::i16_abs_sub(self.magnitude, magnitude) as usize; - self.digits.truncate(digits_to_retain); + let digits_to_retain = crate::ops::i16_abs_sub(self.magnitude, position) + 1; + self.digits.truncate(digits_to_retain as usize); // 3b. Truncate to the previous multiple. match increment { @@ -1125,7 +1146,7 @@ impl FixedDecimal { } RoundingIncrement::MultiplesOf25 => { // Extend with zeroes to have the correct trailing digits. - self.digits.resize(digits_to_retain, 0); + self.digits.resize(digits_to_retain as usize, 0); let Some((last_digit, digits)) = self.digits.split_last_mut() else { // Unreachable. @@ -3659,7 +3680,27 @@ fn test_rounding_increment() { dec.trunc_to_increment(0, RoundingIncrement::MultiplesOf25); assert_eq!("0", dec.to_string()); - let mut dec = FixedDecimal::from_str("0.3").unwrap(); - dec.trunc_to_increment(-1, RoundingIncrement::MultiplesOf2); - assert_eq!("0.2", dec.to_string()); + let mut dec = FixedDecimal::from(7).multiplied_pow10(i16::MIN); + dec.trunc_to_increment(i16::MIN, RoundingIncrement::MultiplesOf2); + assert_eq!(FixedDecimal::from(6).multiplied_pow10(i16::MIN), dec); + + let mut dec = FixedDecimal::from(9).multiplied_pow10(i16::MIN); + dec.trunc_to_increment(i16::MIN, RoundingIncrement::MultiplesOf5); + assert_eq!(FixedDecimal::from(5).multiplied_pow10(i16::MIN), dec); + + let mut dec = FixedDecimal::from(70).multiplied_pow10(i16::MIN); + dec.trunc_to_increment(i16::MIN, RoundingIncrement::MultiplesOf25); + assert_eq!(FixedDecimal::from(50).multiplied_pow10(i16::MIN), dec); + + let mut dec = FixedDecimal::from(7).multiplied_pow10(i16::MAX); + dec.trunc_to_increment(i16::MAX, RoundingIncrement::MultiplesOf2); + assert_eq!(FixedDecimal::from(6).multiplied_pow10(i16::MAX), dec); + + let mut dec = FixedDecimal::from(7).multiplied_pow10(i16::MAX); + dec.trunc_to_increment(i16::MAX, RoundingIncrement::MultiplesOf5); + assert_eq!(FixedDecimal::from(5).multiplied_pow10(i16::MAX), dec); + + let mut dec = FixedDecimal::from(7).multiplied_pow10(i16::MAX); + dec.trunc_to_increment(i16::MAX, RoundingIncrement::MultiplesOf25); + assert_eq!(FixedDecimal::from(0).multiplied_pow10(i16::MAX), dec); } From b2ff771b6e51d85b01ce97b35c35c1ea8ffe76d0 Mon Sep 17 00:00:00 2001 From: jedel1043 Date: Mon, 30 Oct 2023 22:35:35 -0600 Subject: [PATCH 3/3] Add `debug_assert` on unreachable paths --- utils/fixed_decimal/src/decimal.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/utils/fixed_decimal/src/decimal.rs b/utils/fixed_decimal/src/decimal.rs index d7366c09cb8..903c978c0f9 100644 --- a/utils/fixed_decimal/src/decimal.rs +++ b/utils/fixed_decimal/src/decimal.rs @@ -1128,7 +1128,7 @@ impl FixedDecimal { } RoundingIncrement::MultiplesOf2 => { let Some(last_digit) = self.digits.last_mut() else { - // Unreachable + debug_assert!(false, "`self.digits` should have at least a digit"); return; }; @@ -1138,7 +1138,7 @@ impl FixedDecimal { } RoundingIncrement::MultiplesOf5 => { let Some(last_digit) = self.digits.last_mut() else { - // Unreachable + debug_assert!(false, "`self.digits` should have at least a digit"); return; }; @@ -1149,7 +1149,7 @@ impl FixedDecimal { self.digits.resize(digits_to_retain as usize, 0); let Some((last_digit, digits)) = self.digits.split_last_mut() else { - // Unreachable. + debug_assert!(false, "`self.digits` should have at least a digit"); return; };