Skip to content

Commit c6ff3c6

Browse files
Merge branch 'main' into release
2 parents 2f6c370 + 7e8bb94 commit c6ff3c6

File tree

5 files changed

+296
-4
lines changed

5 files changed

+296
-4
lines changed

Directory.Build.props

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
<Project>
22
<PropertyGroup>
3+
<Version>12.1.0</Version>
4+
<PackageVersion>12.1.0</PackageVersion>
5+
<AssemblyVersion>12.1.0</AssemblyVersion>
36
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
47
<LangVersion>13</LangVersion>
58
<Nullable>enable</Nullable>
@@ -8,8 +11,5 @@
811
<NeutralLanguage>en</NeutralLanguage>
912
<Copyright>Copyright © ONIXLabs 2020</Copyright>
1013
<RepositoryUrl>https://github.com/onix-labs/onixlabs-dotnet</RepositoryUrl>
11-
<Version>12.0.0</Version>
12-
<PackageVersion>12.0.0</PackageVersion>
13-
<AssemblyVersion>12.0.0</AssemblyVersion>
1414
</PropertyGroup>
1515
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// Copyright 2020-2025 ONIXLabs
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
using System;
16+
using Xunit;
17+
18+
namespace OnixLabs.Numerics.UnitTests;
19+
20+
public sealed class GenericMathPow10Tests
21+
{
22+
[Theory(DisplayName = "GenericMath.Pow10 should produce the expected result (Int32)")]
23+
[InlineData(0, 1)]
24+
[InlineData(1, 10)]
25+
[InlineData(2, 100)]
26+
[InlineData(3, 1000)]
27+
[InlineData(4, 10000)]
28+
[InlineData(5, 100000)]
29+
[InlineData(6, 1000000)]
30+
[InlineData(9, 1000000000)]
31+
public void GenericMathPow10ShouldProduceExpectedResultInt32(int exponent, int expected)
32+
{
33+
// Given / When
34+
int result = GenericMath.Pow10<int>(exponent);
35+
36+
// Then
37+
Assert.Equal(expected, result);
38+
}
39+
40+
[Theory(DisplayName = "GenericMath.Pow10 should produce the expected result (Int64)")]
41+
[InlineData(0, 1L)]
42+
[InlineData(1, 10L)]
43+
[InlineData(2, 100L)]
44+
[InlineData(10, 10000000000L)]
45+
[InlineData(15, 1000000000000000L)]
46+
public void GenericMathPow10ShouldProduceExpectedResultInt64(int exponent, long expected)
47+
{
48+
// Given / When
49+
long result = GenericMath.Pow10<long>(exponent);
50+
51+
// Then
52+
Assert.Equal(expected, result);
53+
}
54+
55+
[Theory(DisplayName = "GenericMath.Pow10 should produce the expected result (Double)")]
56+
[InlineData(0, 1.0)]
57+
[InlineData(3, 1000.0)]
58+
[InlineData(6, 1e6)]
59+
[InlineData(9, 1e9)]
60+
[InlineData(15, 1e15)]
61+
public void GenericMathPow10ShouldProduceExpectedResultDouble(int exponent, double expected)
62+
{
63+
// Given / When
64+
double result = GenericMath.Pow10<double>(exponent);
65+
66+
// Then
67+
Assert.Equal(expected, result, precision: 10);
68+
}
69+
70+
[Theory(DisplayName = "GenericMath.Pow10 should produce the expected result (Decimal)")]
71+
[InlineData(0, "1")]
72+
[InlineData(1, "10")]
73+
[InlineData(5, "100000")]
74+
[InlineData(10, "10000000000")]
75+
public void GenericMathPow10ShouldProduceExpectedResultDecimal(int exponent, string expectedStr)
76+
{
77+
// Given / When
78+
decimal expected = decimal.Parse(expectedStr);
79+
decimal result = GenericMath.Pow10<decimal>(exponent);
80+
81+
// Then
82+
Assert.Equal(expected, result);
83+
}
84+
85+
[Fact(DisplayName = "GenericMath.Pow10 should throw ArgumentException when the exponent is negative.")]
86+
public void GenericMathPow10ShouldThrowArgumentExceptionWhenExponentNegative()
87+
{
88+
// Given / When
89+
Exception exception = Assert.Throws<ArgumentException>(() => GenericMath.Pow10<int>(-1));
90+
91+
// Then
92+
Assert.Contains("Exponent must be greater", exception.Message);
93+
}
94+
}

OnixLabs.Numerics.UnitTests/NumericsExtensionsTests.cs

+101
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,107 @@ public void DecimalGetUnscaledValueShouldProduceExpectedResultValueAndScale(int
8080
Assert.Equal(expected, actual);
8181
}
8282

83+
[Theory(DisplayName = "Decimal.SetScale should preserve or pad when scale is less or equal")]
84+
[InlineData("123.0", 2, "123.00")]
85+
[InlineData("123.00", 2, "123.00")]
86+
[InlineData("123.000", 2, "123.00")]
87+
[InlineData("0.0", 2, "0.00")]
88+
[InlineData("123.12", 2, "123.12")]
89+
public void DecimalSetScaleShouldPreserveOrPadWhenScaleIsLessOrEqual(string inputStr, int scale, string expectedStr)
90+
{
91+
// Given
92+
decimal input = decimal.Parse(inputStr);
93+
decimal expected = decimal.Parse(expectedStr);
94+
95+
// When
96+
decimal result = input.SetScale(scale);
97+
98+
// Then
99+
Assert.Equal(expected, result);
100+
}
101+
102+
[Theory(DisplayName = "Decimal.SetScale should truncate when no precision loss")]
103+
[InlineData("123.1200", 2, "123.12")]
104+
[InlineData("0.1000", 1, "0.1")]
105+
[InlineData("999.0000", 3, "999.000")]
106+
public void DecimalSetScaleShouldTruncateWhenNoPrecisionLoss(string inputStr, int scale, string expectedStr)
107+
{
108+
// Given
109+
decimal input = decimal.Parse(inputStr);
110+
decimal expected = decimal.Parse(expectedStr);
111+
112+
// When
113+
decimal result = input.SetScale(scale);
114+
115+
// Then
116+
Assert.Equal(expected, result);
117+
}
118+
119+
[Theory(DisplayName = "Decimal.SetScale should throw when truncation would lose precision")]
120+
[InlineData("123.456", 2)]
121+
[InlineData("1.001", 2)]
122+
[InlineData("0.123456789", 5)]
123+
public void DecimalSetScaleShouldThrowWhenTruncationWouldLosePrecision(string inputStr, int scale)
124+
{
125+
// Given
126+
decimal input = decimal.Parse(inputStr);
127+
128+
// When / Then
129+
Assert.Throws<InvalidOperationException>(() => input.SetScale(scale));
130+
}
131+
132+
[Theory(DisplayName = "Decimal.SetScale(rounding) should apply correct rounding")]
133+
[InlineData("123.456", 2, MidpointRounding.AwayFromZero, "123.46")]
134+
[InlineData("123.454", 2, MidpointRounding.AwayFromZero, "123.45")]
135+
[InlineData("123.455", 2, MidpointRounding.ToZero, "123.45")]
136+
[InlineData("123.455", 2, MidpointRounding.ToEven, "123.46")]
137+
[InlineData("0.125", 2, MidpointRounding.ToEven, "0.12")]
138+
[InlineData("0.135", 2, MidpointRounding.ToEven, "0.14")]
139+
public void DecimalSetScaleWithRoundingShouldApplyCorrectRounding(string inputStr, int scale, MidpointRounding mode, string expectedStr)
140+
{
141+
// Given
142+
decimal input = decimal.Parse(inputStr);
143+
decimal expected = decimal.Parse(expectedStr);
144+
145+
// When
146+
decimal result = input.SetScale(scale, mode);
147+
148+
// Then
149+
Assert.Equal(expected, result);
150+
}
151+
152+
[Theory(DisplayName = "Decimal.SetScale(rounding) should truncate when no precision loss")]
153+
[InlineData("123.0000", 2, MidpointRounding.AwayFromZero, "123.00")]
154+
[InlineData("1.000", 1, MidpointRounding.ToEven, "1.0")]
155+
public void DecimalSetScaleWithRoundingShouldTruncateWhenNoPrecisionLoss(string inputStr, int scale, MidpointRounding mode, string expectedStr)
156+
{
157+
// Given
158+
decimal input = decimal.Parse(inputStr);
159+
decimal expected = decimal.Parse(expectedStr);
160+
161+
// When
162+
decimal result = input.SetScale(scale, mode);
163+
164+
// Then
165+
Assert.Equal(expected, result);
166+
}
167+
168+
[Fact(DisplayName = "Decimal.SetScale should throw when scale is negative")]
169+
public void DecimalSetScaleShouldThrowWhenScaleIsNegative()
170+
{
171+
// Given / When / Then
172+
Exception exception = Assert.Throws<ArgumentException>(() => 123.45m.SetScale(-1));
173+
Assert.Contains("Scale must be greater than, or equal to zero.", exception.Message);
174+
}
175+
176+
[Fact(DisplayName = "Decimal.SetScale(rounding) should throw when scale is negative")]
177+
public void DecimalSetScaleWithRoundingShouldThrowWhenScaleIsNegative()
178+
{
179+
// Given / When / Then
180+
Exception exception = Assert.Throws<ArgumentException>(() => 123.45m.SetScale(-1, MidpointRounding.AwayFromZero));
181+
Assert.Contains("Scale must be greater than, or equal to zero.", exception.Message);
182+
}
183+
83184
[Theory(DisplayName = "INumber<T>.IsBetween should produce the expected result")]
84185
[InlineData(0, 0, 0, true)]
85186
[InlineData(0, 0, 1, true)]

OnixLabs.Numerics/GenericMath.cs

+31-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
using System;
1516
using System.Globalization;
1617
using System.Numerics;
1718

@@ -41,7 +42,7 @@ public static BigInteger Factorial<T>(T value) where T : IBinaryInteger<T>
4142
{
4243
Require(value >= T.Zero, "Value must be greater than or equal to zero.");
4344

44-
if(value <= T.One) return BigInteger.One;
45+
if (value <= T.One) return BigInteger.One;
4546

4647
BigInteger result = BigInteger.One;
4748

@@ -71,4 +72,33 @@ public static int IntegerLength<T>(T value) where T : INumberBase<T>
7172
/// <typeparam name="T">The underlying <see cref="INumber{TSelf}"/> type.</typeparam>
7273
/// <returns>Returns the minimum and maximum values from the specified left-hand and right-hand values.</returns>
7374
public static (T Min, T Max) MinMax<T>(T left, T right) where T : INumber<T> => (T.Min(left, right), T.Max(left, right));
75+
76+
/// <summary>
77+
/// Computes 10 raised to the power of a non-negative integer exponent using exponentiation by squaring, for any numeric type implementing <see cref="INumber{T}"/>.
78+
/// </summary>
79+
/// <typeparam name="T">The numeric type. Must implement <see cref="INumber{T}"/>.</typeparam>
80+
/// <param name="exponent">The exponent to raise 10 to. Must be greater than or equal to zero.</param>
81+
/// <returns>A value of type <typeparamref name="T"/> equal to 10 raised to the power of <paramref name="exponent"/>.</returns>
82+
/// <exception cref="ArgumentException"> if <paramref name="exponent"/> is less than zero.</exception>
83+
public static T Pow10<T>(int exponent) where T : INumber<T>
84+
{
85+
Require(exponent >= 0, "Exponent must be greater than, or equal to zero.", nameof(exponent));
86+
87+
if (exponent == 0)
88+
return T.One;
89+
90+
T result = T.One;
91+
T baseValue = T.CreateChecked(10);
92+
93+
while (exponent > 0)
94+
{
95+
if ((exponent & 1) == 1)
96+
result *= baseValue;
97+
98+
baseValue *= baseValue;
99+
exponent >>= 1;
100+
}
101+
102+
return result;
103+
}
74104
}

OnixLabs.Numerics/NumericsExtensions.cs

+67
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,73 @@ public static BigInteger GetUnscaledValue(this decimal value)
5151
return decimal.IsPositive(value) ? result : -result;
5252
}
5353

54+
/// <summary>
55+
/// Sets the scale (number of digits after the decimal point) of the current <see cref="decimal"/> value.
56+
/// <remarks>
57+
/// If the scale of the current <see cref="decimal"/> value is less than the specified scale, then the scale will be padded with zeroes.
58+
/// If the scale of the current <see cref="decimal"/> value is greater that the specified scale, then the scale will be truncated,
59+
/// provided that there is no loss of precision; otherwise, <see cref="InvalidOperationException"/> will be thrown.
60+
/// </remarks>
61+
/// </summary>
62+
/// <param name="value">The decimal value to adjust.</param>
63+
/// <param name="scale">The desired, non-negative scale.</param>
64+
/// <returns>A new <see cref="decimal"/> with the exact specified scale.</returns>
65+
/// <exception cref="InvalidOperationException"> if reducing the scale would result in a loss of precision.</exception>
66+
/// <exception cref="ArgumentException"> if <paramref name="scale"/> is negative.</exception>
67+
public static decimal SetScale(this decimal value, int scale)
68+
{
69+
Require(scale >= 0, "Scale must be greater than, or equal to zero.", nameof(scale));
70+
71+
if (value.Scale == scale)
72+
return value;
73+
74+
if (value.Scale < scale)
75+
{
76+
decimal factor = GenericMath.Pow10<decimal>(scale - value.Scale);
77+
return value * factor / factor;
78+
}
79+
80+
decimal pow10 = GenericMath.Pow10<decimal>(scale);
81+
decimal truncated = Math.Truncate(value * pow10) / pow10;
82+
83+
if (value == truncated)
84+
return truncated;
85+
86+
throw new InvalidOperationException($"Cannot reduce scale without losing precision: {value}");
87+
}
88+
89+
/// <summary>
90+
/// Sets the scale (number of digits after the decimal point) of the current <see cref="decimal"/> value.
91+
/// <remarks>
92+
/// If the scale of the current <see cref="decimal"/> value is less than the specified scale, then the scale will be padded with zeroes.
93+
/// If the scale of the current <see cref="decimal"/> value is greater that the specified scale, then the scale will be truncated,
94+
/// provided that there is no loss of precision; otherwise, the value is rounded using the specified <see cref="MidpointRounding"/> mode.
95+
/// </remarks>
96+
/// </summary>
97+
/// <param name="value">The decimal value to adjust.</param>
98+
/// <param name="scale">The desired scale (number of decimal digits). Must be non-negative.</param>
99+
/// <param name="mode">The rounding strategy to apply if the scale must be reduced with precision loss.</param>
100+
/// <returns>A new <see cref="decimal"/> with the exact specified scale.</returns>
101+
/// <exception cref="ArgumentException"> if <paramref name="scale"/> is negative.</exception>
102+
public static decimal SetScale(this decimal value, int scale, MidpointRounding mode)
103+
{
104+
Require(scale >= 0, "Scale must be greater than, or equal to zero.", nameof(scale));
105+
106+
if (value.Scale == scale)
107+
return value;
108+
109+
if (value.Scale < scale)
110+
{
111+
decimal factor = GenericMath.Pow10<decimal>(scale - value.Scale);
112+
return value * factor / factor;
113+
}
114+
115+
decimal pow10 = GenericMath.Pow10<decimal>(scale);
116+
decimal truncated = Math.Truncate(value * pow10) / pow10;
117+
118+
return value == truncated ? truncated : Math.Round(value, scale, mode);
119+
}
120+
54121
/// <summary>
55122
/// Gets the current value as an unscaled integer.
56123
/// </summary>

0 commit comments

Comments
 (0)