Oh-my-decimal is a weird Frankenstein's monster that combines:
- decimal from IEEE 754 2008
Swift.Double
If Swift.Double overrides the standard then Swift.Double behavior is implemented with the following exceptions:
-
sign of
sNaNfollows the same rules as sign ofqNaN- this is not the case forSwift.Double. Note that while the creation ofsNaN(copy,copySign,scaleBetc.) will givesNaN, most of the arithmetic operations will still returnqNaNwithinvalidOperationflag raised. -
value returned by the
significandproperty is always positive. In Swift(-Double.nan).significandwill return-nan. This is needed to make thescaleBaxiom work:let y = F(sign: x.sign, exponent: x.exponent, significand: x.significand)thenxandyshould be equal. ObviouslyNaNsare never equal (I'm not sure why documentation is written in this way), but we will have the same sign, signaling bit and payload. Note that:oh-my-decimaldoes not implementFloatingPointprotocol from which this requirement comes from.- both
oh-my-decimalandSwift.Doublewill returnsNaNif thesignificandargument ofscaleBissNaN. Standard would returnqNaNand raiseinvalidOperation.
-
minimum/maximumoh-my-decimalimplements the standard 2008: if one of the operands issNaNthen the result is aNaNwithinvalidOperationraised.- standard 2019 introduces new operations as there was a whole debate about the corner cases of 2008.
- Swift documentation says: "If both x and y are NaN, or either x or y is a signaling NaN, the result is NaN", with a link to the standard 2008. In practice for
sNaNit returns the non-NaN operand.
-
no
.awayFromZerorounding - this is trivial to implement, but only speleotrove contains tests for it (they call itround-up). Since rounding is present in most of the operations, a single test suite is not enough to be fully sure that everything works correctly. Inoh-my-decimalmost important things are covered by: Intel, Speleotrove, Hossam A. H. Fahmy and oh-my-decimal-tests tests. Also, IEEE 754 does not require this rounding mode, so 🤷. -
missing protocols:
FloatingPoint- we have our ownDecimalFloatingPoint.ExpressibleByFloatLiteral- Swift converts toFloat80/Doubleand then converts to a number. This conversion may not be exact, so it is basically a random number generator.Strideable- really quickly it would break the Sterbenz lemma:y/2 < x < 2y. What is the distance betweengreatestFiniteMagnitudeandleastNormalMagnitude?Random- apart from a few specific input ranges it would not do what user wants:- simple random between 0 and 10 would be skewed towards smaller numbers because more of them are representable (tons of possible negative exponents).
- if we generated truly random (infinitely precise) value and rounded then bigger numbers would be more common (they have bigger ulp).
Examples (Intel, this library was not tested on Apple silicon):
// Container for IEEE 754 flags: inexact, invalidOperation etc.
var status = DecimalStatus()
// Standard: nan + invalidOperation
// Swift: nan
print(Decimal64.signalingNaN.nextUp(status: &status)) // nan + invalidOperation 🟢
print(Double.signalingNaN.nextUp) // nan 🟢
status.clearAll()
// Standard: nan + invalidOperation
// Swift: nan
print(Decimal64.signalingNaN + Decimal64.signalingNaN) // nan 🟢
print(Decimal64.signalingNaN.adding(Decimal64.signalingNaN, rounding: .towardZero, status: &status)) // nan + invalidOperation 🟢
print(Double.signalingNaN + Double.signalingNaN) // nan 🟢
status.clearAll()
// Standard: nan + invalidOperation
// Swift: https://www.youtube.com/watch?v=nptj1uWFy5s
print((-Decimal64.signalingNaN).magnitude) // snan 🔴
print((-Double.signalingNaN).magnitude) // snan 🔴
// 'scaleB' axiom
let d1 = -Decimal64.nan
print(Decimal64(sign: d1.sign, exponent: 0, significand: d1.significand)) // -nan 🟢
let d2 = -Double.nan
print(Double(sign: d2.sign, exponent: 0, significand: d2.significand)) // nan 🔴
// Standard: canonicalized number
// Swift: number
print(Decimal64.minimum(Decimal64.nan, 1, status: &status)) // 1E+0 🟢
print(Double.minimum(Double.nan, 1)) // 1.0 🟢
// Standard: nan + invalidOperation
// Swift: number
print(Decimal64.minimum(1, Decimal64.signalingNaN, status: &status)) // nan + invalidOperation 🟢
print(Double.minimum(1, Double.signalingNaN)) // 1.0 🔴mr-darcy(this branch) - Swift implementation.mr-bingley- wrapper for Intel library. It haspow, butDecimalStatusis not publicly available.
Sources/Decimal
-
Generated- code generated by Python scripts.Decimal32Decimal64Decimal128DecimalFloatingPoint- dem protocol.
-
DecimalMixin- internal protocol on which every operation is defined. All of the methods fromDecimalXXtypes will eventually call a method fromDecimalMixin. Methods start with '_' to avoid name clashes with thepublicmethods exported fromDecimalXXtypes. -
DecimalFloatingPointRoundingRule- similar toFloatingPointRoundingRulebut withoutawayFromZero- not required by IEEE 754, not enough test cases to guarantee correctness. -
DecimalStatus- holds IEEE 754 flags:isInvalidOperation,isDivisionByZero,isOverflow,isUnderflow, andisInexact. Lightweight, you can create as many statuses as you want, they are completely independent. Usually the last argument:public func adding( _ other: Self, rounding: DecimalFloatingPointRoundingRule, status: inout DecimalStatus ) -> Self { … }
Sources/test-hossam-fahmy - app to run Hossam-Fahmy-tests and Oh-my-decimal-tests. Use make test-hossam-fahmy to run in RELEASE mode. It finishes in ~10min on Intel Pentium G4560. Probably faster if you have better CPU, this thing eats CPU cores like candies.
Tests/DecimalTests
Generated- unit tests generated by Python scripts.Intel - generated- unit tests generated from Intel test suite (Test-suites/IntelRDFPMathLib20U2).Speleotrove - generated- unit tests generated from Speleotrove test suite (Test-suites/speleotrove-dectest).
Test-suites
-
IntelRDFPMathLib20U2- put Intel decimal here. Or not. This is only used for generating unit tests. Usemake gento re-generate andmake testto run. -
speleotrove-dectest- put Speleotrove test suite here. Or not. This is only used for generating unit tests. Usemake gento re-generate andmake testto run. -
Hossam-Fahmy-tests- put Hossam A. H. Fahmy test suite here. Usemake test-hossam-fahmyto run. -
Oh-my-decimal-tests- put oh-my-decimal-test-suite here. Usemake test-hossam-fahmyto run.
Scripts - Python code generators. Use make gen to run.
make build- …?make test- run unit tests.make test-hossam-fahmy- runHossam-Fahmy-testsandOh-my-decimal-teststests.make x- run a subset of unit tests. Remember to modify theMakefileto re-define what this subset is.make run- runExperiments.test_mainunit test. This is the “playground” used for ad-hoc tests when writing the library.make gen- run Python scripts to generate code.make intel-copy- create a directory with links to the most important Intel files.
Most of the time the workflow is: make x, make x, make x, make test, and finally make test-hossam-fahmy.
- HexCharacter -
Swift.Doubleactually has this, but it is not widely used and I am not into writing parsers.- convertFrom
- convertTo
- compareQuiet
- Ordered
- Unordered
- compareSignaling - tiny modification of
compareQuietthat can be added inextensionif user so desires.- Equal
- NotEqual
- Greater
- GreaterEqual
- GreaterUnordered
- NotGreater
- Less
- LessEqual
- LessUnordered
- NotLess
Side note: for compare operations you want to read IEEE 754 2019 instead of 2008. The content is the same, but the language is more approachable.
| IEEE 754 | Oh-my-decimal | |
|---|---|---|
Unary +Unary -magnitudecopycopySigninit(sign:exponent:significand:rounding:status:) (scaleB) |
sNaN returns NaN and raises invalidOperation. |
sNaN returns sNaN, no flags raised. |
Maybe something else, but in general it follows Swift.Double, so you know what to expect.
| Operation | Reason |
|---|---|
Cute operators like * or /(maybe even + or -) |
Use the overloads with the rounding argument. Bonus points for using status. |
addingProduct(fused multiply add, FMA) |
Most of the time you actually want the intermediate rounding. |
| Binary floating point interop | Bullies from IEEE forced us to implement this (formatOf-convertFormat(source) operation). NEVER EVER USE THIS THINGIE. MASSIVE 🚩 WHEN YOU SEE SOMEBODY DOING THIS. |
Decimal128._UInt128 |
This is not a general purpose UInt128. It works for Decimal, but it may not work in your specific case. No guarantees. |
-
round(decimalDigitCount:)=quantizedlet d = Decimal128("123.456789")! let precision = Decimal128("0.01")! var status = DecimalStatus() let result = d.quantized(to: precision, rounding: .towardZero, status: &status) print(result, status) // 12345E-2, isInexact status.clear(.isInexact) // Inexact flag will not be raised if the result is… well… exact. let d2 = Decimal128("123.450000")! let result2 = d2.quantized(to: precision, rounding: .towardZero, status: &status) print(result2, status) // 12345E-2, empty // But remember that you can't store more digits than supported by a given format. // Doing so will result in 'nan' with 'InvalidOperation' raised. // For example 'Decimal32' can store only 7 significand digits: let d32 = Decimal32("1234567")! let precision32 = Decimal32("0.1")! let result32 = d32.quantized(to: precision32, rounding: .towardZero, status: &status) print(result32, status) // nan, isInvalidOperation
-
multiply by power of 10 =
init(sign:exponent:significand:rounding:status:)(also known asscaleB)let d = Decimal64("1234")! var status = DecimalStatus() let result = Decimal64(sign: .plus, exponent: 20, significand: d, rounding: .towardZero, status: &status) print(d) // 1234E+0 print(result, status) // 1234E+20, DecimalStatus()
Oh-my-decimal is feature complete, no new functionalities are planned. At some point I may add pow with Int argument, but probably not…
Do not submit any of the following PRs - they will NOT be merged:
powwithIntargument - I want to write this myself.- PeRfOrMaNcE - especially any of the
@inlinable/usableFromInlinethings. Just don't.
- 2-space indents and no tabs at all
- 80 characters per line
- Required
selfin methods and computed properties- All of the other method arguments are named, so we will require it for this one.
Self/type namefor static methods is recommended, but not required.- I’m sure that they will depreciate the implicit
selfin the next major Swift version 🤞. All of that source breakage is completely justified.
- No whitespace at the end of the line
- Some editors may remove it as a matter of routine and we don’t want weird git diffs.
- (pet peeve) Try to introduce a named variable for every
ifcondition.- You can use a single logical operator - something like
if !isPrincessorif isDisnepCharacter && isPrincessis allowed. - Do not use
&&and||in the same expression, create a variable for one of them. - If you need parens then it is already too complicated.
- You can use a single logical operator - something like
Oh-my-decimal is distributed under the “GNU General Public License”. You are NOT permitted to copy the code and distribute it solely under MIT.
Tests/DecimalTests/Intel - generated is generated from Intel code. This makes it dual-licensed. Intel license is available in LICENSE-Intel file.
Tests/DecimalTests/Speleotrove - generated is generated from Speleotrove test suite. This makes it dual-licensed. Speleotrove license is available in LICENSE-speleotrove-dectest file.
Hossam-Fahmy-tests are not a part of this repository, but just for completeness their license is available in LICENSE-Hossam-Fahmy-tests file.