Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Operations with unreduced fractions #65

Merged
merged 25 commits into from
May 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e9d6c46
operations with unreduced fractions should now produce unreduced frac…
lipchev May 12, 2024
3c2ded8
replicated the BasicMath bencharks using their non-reduced counterparts
lipchev May 12, 2024
6093307
added short-path testing for-1 and 0 to ReduceTerms
lipchev May 12, 2024
ed31261
-one check too many: 0 % 10 is 10
lipchev May 12, 2024
c71d513
- fixed the issues with ToDecimalWithTrailingZeros
lipchev May 18, 2024
65f1ca0
added support for creating non-reduced fractions when parsing a decim…
lipchev May 19, 2024
9d5a5c2
Replace methods with default parameters with two methods to remain AB…
danm-de May 19, 2024
1b196ba
- Added missing FromString overload.
danm-de May 19, 2024
2069684
Corrected unit test for JsonFractionConverter (breaking change)
danm-de May 19, 2024
2bd6d07
Move FromString method to a separate file: Fraction.ConvertFromString…
danm-de May 19, 2024
f3cbf8f
Move FromDouble method to a separate file: Fraction.ConvertFromDouble…
danm-de May 19, 2024
c708381
Move FromDecimal method to a separate file: Fraction.ConvertFromDecim…
danm-de May 19, 2024
a9e4ea4
Fix nullability (FromString) + compiler warnings in FractionTypeConve…
danm-de May 19, 2024
ab41e0e
Only one ternary conditional operator in a (single) statement.
danm-de May 19, 2024
870af26
Cleanup code - InvalidNumberException is not longer used.
danm-de May 19, 2024
2d81d10
- Cleanup xmldocs
danm-de May 19, 2024
a04dfff
Fix XML-doc comments
danm-de May 19, 2024
07bef43
Code cleanup - remove compiler warnings
danm-de May 19, 2024
54b390b
Fix XML-doc comments
danm-de May 19, 2024
ffda4fe
update documentation
danm-de May 19, 2024
21d7ab2
Fix readme (terms are reduced when using multiplication)
danm-de May 19, 2024
291c28e
Added line breaks to avoid a single line displayed on github.com.
danm-de May 19, 2024
a1f4aa2
Try Mathjax new line symbol to insert line breaks.
danm-de May 19, 2024
f52d1a4
Another try to introduce line breaks in a MathJax formular (latex)
danm-de May 19, 2024
ba15cef
Added 2 spaces in order to get a linebreak (please?)
danm-de May 19, 2024
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
7 changes: 6 additions & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,11 @@ csharp_style_prefer_extended_property_pattern = true:suggestion

dotnet_style_operator_placement_when_wrapping = beginning_of_line

# Suppress warnings
# CA1510: Use ArgumentNullException throw helper
# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1510
dotnet_diagnostic.CA1510.severity = none

# "This." and "Me." qualifiers
# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#this_and_me
dotnet_style_qualification_for_field = false:suggestion
Expand Down Expand Up @@ -341,7 +346,7 @@ dotnet_naming_rule.non_private_readonly_fields_must_be_pascal_case.style = pasca
# Static readonly fields must be PascalCase
dotnet_naming_rule.static_readonly_fields_must_be_pascal_case.severity = warning
dotnet_naming_rule.static_readonly_fields_must_be_pascal_case.symbols = static_readonly_fields
dotnet_naming_rule.static_readonly_fields_must_be_pascal_case.style = pascal_case
dotnet_naming_rule.static_readonly_fields_must_be_pascal_case.style = prefix_underscore_camel_case
# Private fields must be camelCase with underscore
dotnet_naming_rule.private_instance_fields_must_be_camel_case_with_underscore.severity = warning
dotnet_naming_rule.private_instance_fields_must_be_camel_case_with_underscore.symbols = private_fields
Expand Down
4 changes: 3 additions & 1 deletion Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
- New properties IsNaN, IsInfinity, IsPositiveInfinity, IsNegativeInfinity by https://github.com/lipchev
- Adding a debugger display proxy by https://github.com/lipchev
- Various methods were optimized by https://github.com/lipchev
- Use the potential of Span<T> where sensible and possible (TryParse, FromDecimal) by https://github.com/lipchev
- Use the potential of Span&lt;T&gt; where sensible and possible (TryParse, FromDecimal) by https://github.com/lipchev
- No longer reduce non-normalized fractions when used in mathematical operations - by https://github.com/lipchev

### Breaking changes

- Mathematical operations no longer automatically generate normalized fractions if one of the operands is an improper (i.e. non-normalized) fraction. This has an impact on your calculations, especially if you have used the `JsonFractionConverter` with default settings. In such cases, deserialized fractions create improper fractions, which can lead to changed behavior when calling `Equals` and `ToString`. Please refer the README section [Working with non-normalized fractions](Readme.md#working-with-non-normalized-fractions)
- The standard function `ToString()` now depends on the active culture (`CultureInfo.CurrentCulture`). The reason is that NaN and Infinity should be displayed in the system language or the corresponding symbol should be used.
- Argument name for `Fraction.TryParse(..)` changed from `fractionString` to `value`.
- A fraction of 0/0 no longer has the value 0, but means NaN (not a number). Any fraction in the form x/0 is no longer a valid number. A denominator with the value 0 corresponds to, depending on the numerator, NaN, PositiveInfinity or NegativeInfinity.
Expand Down
3 changes: 3 additions & 0 deletions Fractions.sln
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,15 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Results", "Results", "{FCF0
ProjectSection(SolutionItems) = preProject
benchmarks\results\Fractions.Benchmarks.BasicMathBenchmarks-report-github.md = benchmarks\results\Fractions.Benchmarks.BasicMathBenchmarks-report-github.md
benchmarks\results\Fractions.Benchmarks.ComparisonBenchmarks-report-github.md = benchmarks\results\Fractions.Benchmarks.ComparisonBenchmarks-report-github.md
benchmarks\results\Fractions.Benchmarks.ConsecutiveMathOperationBenchmarks-report-github.md = benchmarks\results\Fractions.Benchmarks.ConsecutiveMathOperationBenchmarks-report-github.md
benchmarks\results\Fractions.Benchmarks.ConstructorBenchmarks-report-github.md = benchmarks\results\Fractions.Benchmarks.ConstructorBenchmarks-report-github.md
benchmarks\results\Fractions.Benchmarks.ConvertToFractionBenchmarks-report-github.md = benchmarks\results\Fractions.Benchmarks.ConvertToFractionBenchmarks-report-github.md
benchmarks\results\Fractions.Benchmarks.ConvertToNumberBenchmarks-report-github.md = benchmarks\results\Fractions.Benchmarks.ConvertToNumberBenchmarks-report-github.md
benchmarks\results\Fractions.Benchmarks.DecimalNotationFormatterBenchmarks-report-github.md = benchmarks\results\Fractions.Benchmarks.DecimalNotationFormatterBenchmarks-report-github.md
benchmarks\results\Fractions.Benchmarks.ExampleScenarioBenchmarks-report-github.md = benchmarks\results\Fractions.Benchmarks.ExampleScenarioBenchmarks-report-github.md
benchmarks\results\Fractions.Benchmarks.FromDoubleBenchmarks-report-github.md = benchmarks\results\Fractions.Benchmarks.FromDoubleBenchmarks-report-github.md
benchmarks\results\Fractions.Benchmarks.FromStringBenchmarks-report-github.md = benchmarks\results\Fractions.Benchmarks.FromStringBenchmarks-report-github.md
benchmarks\results\Fractions.Benchmarks.NonNormalizedMathBenchmarks-report-github.md = benchmarks\results\Fractions.Benchmarks.NonNormalizedMathBenchmarks-report-github.md
benchmarks\results\Fractions.Benchmarks.PowerMathBenchmarks-report-github.md = benchmarks\results\Fractions.Benchmarks.PowerMathBenchmarks-report-github.md
benchmarks\results\Fractions.Benchmarks.ToStringBenchmarks-report-github.md = benchmarks\results\Fractions.Benchmarks.ToStringBenchmarks-report-github.md
EndProjectSection
Expand Down
1 change: 1 addition & 0 deletions Fractions.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=4a98fdf6_002D7d98_002D4f5a_002Dafeb_002Dea44ad98c70c/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"&gt;&lt;ElementKinds&gt;&lt;Kind Name="FIELD" /&gt;&lt;Kind Name="READONLY_FIELD" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /&gt;&lt;/Policy&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=61a991a4_002Dd0a3_002D4d19_002D90a5_002Df8f4d75c30c1/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Any" AccessRightKinds="Any" Description="Local variables"&gt;&lt;ElementKinds&gt;&lt;Kind Name="LOCAL_VARIABLE" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;&lt;/Policy&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=669e5282_002Dfb4b_002D4e90_002D91e7_002D07d269d04b60/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Any" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Constant fields (not private)"&gt;&lt;ElementKinds&gt;&lt;Kind Name="CONSTANT_FIELD" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /&gt;&lt;/Policy&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=7c001be7_002Dcd0e_002D44d1_002D95be_002D73d2c4f1d7bb/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Any" AccessRightKinds="Any" Description="locals_should_be_camel_case"&gt;&lt;ElementKinds&gt;&lt;Kind Name="LOCAL" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb"&gt;&lt;ExtraRule Prefix="" Suffix="" Style="AA_BB" /&gt;&lt;/Policy&gt;&lt;/Policy&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=8a57d8e0_002D07b8_002D4c8b_002D988f_002Db2be05e74804/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Static, Instance" AccessRightKinds="Public" Description="Ignore test classes and test methods"&gt;&lt;ElementKinds&gt;&lt;Kind Name="TEST_TYPE" /&gt;&lt;Kind Name="TEST_MEMBER" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /&gt;&lt;/Policy&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=8a85b61a_002D1024_002D4f87_002Db9ef_002D1fdae19930a1/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Any" AccessRightKinds="Any" Description="Parameters"&gt;&lt;ElementKinds&gt;&lt;Kind Name="PARAMETER" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;&lt;/Policy&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=a4f433b8_002Dabcd_002D4e55_002Da08f_002D82e78cef0f0c/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Any" AccessRightKinds="Any" Description="Local constants"&gt;&lt;ElementKinds&gt;&lt;Kind Name="LOCAL_CONSTANT" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /&gt;&lt;/Policy&gt;</s:String>
Expand Down
62 changes: 54 additions & 8 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ There a three types of constructors available:
- `new Fraction (<numerator>, <denominator>)` using `BigInteger` for numerator and denominator.
- `new Fraction (<numerator>, <denominator>, <reduce>)` using `BigInteger` for numerator and denominator + `bool` to indicate if the resulting fraction shall be normalized (reduced).

> [!IMPORTANT]
> When creating improper fractions (by specifying the parameter `reduce: false`), please be sure to refer to the [Working with non-normalized fractions](#working-with-non-normalized-fractions) and the [Equality operators](#equality-operators) section for more information about the (side) effects.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have no particular preference, but have you considered actually making the Equality/HashCode based on reduced terms? Given that 123.00m is considered equal to 123m I don't think it would be inconsistent to say that 12300/100 is equal to 123/1 .

Copy link
Owner

Choose a reason for hiding this comment

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

I cannot estimate the performance impact on existing code. Currently, most users are likely to use normalized fractions anyway, then the following applies:

If State == FractionState.IsNormalized or _normalizationNotApplied == false is set for both fractions, then the numerator and denominator can simply be compared.

However, the situation is different with non-normalized fractions. The expensive method Reduce would have to be called every time for both GetHashCode and Equals. And we cannot cache the results (readonly struct).

Another question would be: What if for a use case the following applies: $\frac{1}{2} \neq \frac{2}{4}$ (because these are different representations in the user interface)?

Copy link
Owner

Choose a reason for hiding this comment

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

I actually had a long discussion with my colleagues back then. The majority believed that Equals should always be exact. Microsoft does not follow this rule. In our software we have a problem with this:
image

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If the Equals is true then the HashCode should be equal as well, the opposite is not a given.

I cannot estimate the performance impact on existing code. Currently, most users are likely to use normalized fractions anyway, then the following applies:

The GetHashCode is mostly used when checking for stuff like collection.Contains(x) (where collection is actually hash-based)- otherwise we hit the Equals check, which for our scenario can be quite efficient. For actual GetHashCode- related checks, well as you said- it's more likely that somebody is doing the something custom, and can override the way the hash-code is calculated.
The unintentional / unexpected non-equality is what worries me a little- but we'll be fine :)

Copy link
Owner

Choose a reason for hiding this comment

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

I added the .Dump() output in no particular order, but of course: Equals(a,b) == true implied GetHashCode(a) == GetHashCode(b). In the example we have two different time zones - why are both values ​​the same? If I want to know if it is the same point in time, I would apply .ToUniversalTime() to both variables and then compare.

But hey, I'm open to everything. You said you wanted to use the data type in another project (https://github.com/angularsen/UnitsNet right?). What is the opinion there? Is my imagined “use case” perhaps nonsense?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh, we've had lots of issues with Equality contract and how to make it so that "1 kg" == "1000 g". My personal feeling is that making it custom is way easier then making it work for everybody.. I asked the AI some time ago about a design pattern to justify an argument- it cited the principle of least surprise which I quite liked. So as soon as we open the door the optimized operations someone (new) might get surprised.

Copy link
Owner

Choose a reason for hiding this comment

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

POLS, I remember it. Geoffrey James has been quoted quite a bit.

Copy link
Owner

Choose a reason for hiding this comment

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

Let me sleep and think about it one night.

Copy link
Owner

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I don't know whoever thought it was necessary to have the Equals method differ from the == on this one 🤔


### Static creation methods

- `Fraction.FromDecimal(decimal)`
Expand All @@ -54,11 +57,12 @@ There a three types of constructors available:
- `Fraction.TryParse(string, NumberStyles, IFormatProvider, out Fraction)`
- `Fraction.TryParse(ReadOnlySpan<char>, NumberStyles, IFormatProvider, bool, out Fraction)`

### Creation from `double` _without rounding_
### Creation from `double` without rounding

The `double` data type in C# uses a binary floating-point representation, which complies with the [IEC 60559:1989 (IEEE 754)](https://en.wikipedia.org/wiki/IEEE_754) standard for [binary floating-point arithmetic](https://en.wikipedia.org/wiki/Floating-point_arithmetic). This representation can't accurately represent all decimal fractions. For example, the decimal fraction _0.1_ is represented as the repeating binary fraction _.0001100110011..._. As a result, a `double` value can only provide an approximate representation of the decimal number it's intended to represent.

#### Large values in the numerator / denominator

When you convert a `double` to a `Fraction` using the `Fraction.FromDouble` method, the resulting fraction is an exact representation of the `double` value, not the decimal number that the `double` is intended to approximate. This is why you can end up with large numerators and denominators.

```csharp
Expand All @@ -69,6 +73,7 @@ Console.WriteLine(value); // Ouputs "3602879701896397/36028797018963968" which i
The output fraction is an exact representation of the `double` value 0.1, which is actually slightly more than 0.1 due to the limitations of binary floating-point representation.

#### Comparing fractions created with double precision

Using a `Fraction` that was created using this method for strict Equality/Comparison should be avoided. For example:

```csharp
Expand All @@ -80,6 +85,7 @@ Console.WriteLine(fraction1 == fraction2); // Outputs "False"
If you need to compare a `Fraction` created from `double` with others fractions you should either do so by using a tolerance or consider constructing the `Fraction` by [specifying the maximum number of significant digits.](#creation-from-double-with-maximum-number-of-significant-digits)

#### Possible rounding errors near the limits of the double precision

When a `double` value is very close to the limits of its precision, `Fraction.FromDouble(value).ToDouble() == value` might not hold true. This is because the numerator and denominator of the `Fraction` are both very large numbers. When these numbers are converted to `double` for the division operation in the `Fraction.ToDouble` method, they can exceed the precision limit of the `double` type, resulting in a loss of precision.

```csharp
Expand All @@ -100,7 +106,7 @@ Console.WriteLine(value); // Outputs "1/10"

If you care only about minimizing the size of the numerator/denominator, and do not expect to use the fraction in any strict comparison operations, then [creating an approximated fraction](#creation-from-double-with-rounding-to-a-close-approximation) using the `Fraction.FromDoubleRounded(double)` overload should offer the best performance.

### Creation from `double` with rounding to a _close approximation_
### Creation from `double` with rounding to a close approximation

You can use the `Fraction.FromDoubleRounded(double)` method to avoid big numbers in numerator and denominator. Example:

Expand Down Expand Up @@ -129,7 +135,7 @@ The following string patterns can be parsed:
Example:

```csharp
var value = Fraction.FromString("1,5", new CultureInfo("de-DE"))
var value = Fraction.FromString("1,5", CultureInfo.GetCultureInfo("de-DE"))
// Returns 3/2 which is 1.5
Console.WriteLine(value);
```
Expand Down Expand Up @@ -180,8 +186,9 @@ Example:
```csharp
var value = new Fraction(3, 2);
// returns 1 1/2
Console.WriteLine(value.ToString("m", new CultureInfo("de-DE")));
Console.WriteLine(value.ToString("m", CultureInfo.GetCultureInfo("de-DE")));
```

## Decimal Notation Formatter

The `DecimalNotationFormatter` class allows for formatting `Fraction` objects using the standard decimal notation, and the specified format and culture-specific format information.
Expand Down Expand Up @@ -265,6 +272,43 @@ Example:
Console.WriteLine(result);
```

### Working with non-normalized fractions

For performance reasons, as of version 8.0.0, mathematical operations no longer automatically generate normalized fractions if one of the operands is an improper (i.e. non-normalized) fraction. This has an impact on your calculations, especially if you have used the `JsonFractionConverter` with default settings. In such cases, deserialized fractions create improper fractions, which can lead to changed behavior when calling `Equals` and `ToString`.

| Symbol | Description |
| ------ | ---------------------------------------------------------------------------------------------------- |
| $NF$ | Non-normalized (possibly improper) fraction, created with `normalize: false` |
| $F$ | Fraction created with `normalize: true` |
| $⊙$ | Mathematical operation having two operands ($+$, $-$, $*$, $/$, $mod$). |

The following rules apply:

$F ⊙ F = F$
$F ⊙ NF = NF$
$NF ⊙ F = NF$
$NF ⊙ NF = NF$

That said, the following applies for normalized fractions:

```csharp
var a = new Fraction(4, 4, normalize: true); // a is 1/1
var b = new Fraction(2); // b is 2/1 (automatically normalized)
var result = a / b; // result is 1/2
```

$\frac{1}{1}/\frac{2}{1}=\frac{1}{2}$

However, for non-normalized fractions the following applies:

```csharp
var a = new Fraction(4, 4, normalize: false);
var b = new Fraction(2); // b is 2/1 (automatically normalized)
var result = a / b; // result is 4/8
```

$\frac{4}{4}/\frac{2}{1}=\frac{4}{8}$

## Equality operators

`Fraction` implements the following interfaces:
Expand All @@ -273,30 +317,32 @@ Example:
- `IComparable`,
- `IComparable<Fraction>`

Please note that `.Equals(Fraction)` will compare the exact values of numerator and denominator. That said:
Please note that `.Equals(Fraction)` will compare the **exact** values of numerator and denominator. That said:

```csharp
var a = new Fraction(1, 2, normalize: true);
var b = new Fraction(1, 2, normalize: false);
var c = new Fraction(2, 4, normalize: false);
var c = new Fraction(2, 4, normalize: false); // improper fraction

// result1 is true
var result1 = a == a;

// result2 is true
var result2 = a == b;

// result3 is false
// result3 is false!
var result3 = a == c;
```

You have to use `.IsEquivalentTo(Fraction)` if want to test non-normalized fractions for value-equality.
> [!IMPORTANT]
> The `Equals` method does not correspond to the mathematical equality test! You have to use `.IsEquivalentTo(Fraction)` if want to test non-normalized fractions for value-equality.

## Under the hood

The data type stores the numerator and denominator as `BigInteger`. Per default it will reduce fractions to its normalized form during creation. The result of each mathematical operation will be reduced as well. There is a special constructor to create a non-normalized fraction. Be aware that `Equals` relies on normalized values when comparing two different instances.

## Performance considerations

We have a suite of benchmarks that test the performance of various operations in the Fractions library. These benchmarks provide valuable insights into the relative performance of different test cases.
For more detailed information about these benchmarks and how to interpret them, please refer to the [Fractions Benchmarks Readme](./benchmarks/Readme.md) in the benchmarks subfolder.

Expand Down
Loading