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
sNaN
follows the same rules as sign ofqNaN
- this is not the case forSwift.Double
. Note that while the creation ofsNaN
(copy
,copySign
,scaleB
etc.) will givesNaN
, most of the arithmetic operations will still returnqNaN
withinvalidOperation
flag raised. -
value returned by the
significand
property is always positive. In Swift(-Double.nan).significand
will return-nan
. This is needed to make thescaleB
axiom work:let y = F(sign: x.sign, exponent: x.exponent, significand: x.significand)
thenx
andy
should be equal. ObviouslyNaNs
are 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-decimal
does not implementFloatingPoint
protocol from which this requirement comes from.- both
oh-my-decimal
andSwift.Double
will returnsNaN
if thesignificand
argument ofscaleB
issNaN
. Standard would returnqNaN
and raiseinvalidOperation
.
-
minimum/maximum
oh-my-decimal
implements the standard 2008: if one of the operands issNaN
then the result is aNaN
withinvalidOperation
raised.- 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
sNaN
it returns the non-NaN operand.
-
no
.awayFromZero
rounding - 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-decimal
most 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
/Double
and 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 betweengreatestFiniteMagnitude
andleastNormalMagnitude
?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
, butDecimalStatus
is not publicly available.
Sources/Decimal
-
Generated
- code generated by Python scripts.Decimal32
Decimal64
Decimal128
DecimalFloatingPoint
- dem protocol.
-
DecimalMixin
- internal protocol on which every operation is defined. All of the methods fromDecimalXX
types will eventually call a method fromDecimalMixin
. Methods start with '_
' to avoid name clashes with thepublic
methods exported fromDecimalXX
types. -
DecimalFloatingPointRoundingRule
- similar toFloatingPointRoundingRule
but 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 gen
to re-generate andmake test
to run. -
speleotrove-dectest
- put Speleotrove test suite here. Or not. This is only used for generating unit tests. Usemake gen
to re-generate andmake test
to run. -
Hossam-Fahmy-tests
- put Hossam A. H. Fahmy test suite here. Usemake test-hossam-fahmy
to run. -
Oh-my-decimal-tests
- put oh-my-decimal-test-suite here. Usemake test-hossam-fahmy
to run.
Scripts
- Python code generators. Use make gen
to run.
make build
- …?make test
- run unit tests.make test-hossam-fahmy
- runHossam-Fahmy-tests
andOh-my-decimal-tests
tests.make x
- run a subset of unit tests. Remember to modify theMakefile
to re-define what this subset is.make run
- runExperiments.test_main
unit 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.Double
actually has this, but it is not widely used and I am not into writing parsers.- convertFrom
- convertTo
- compareQuiet
- Ordered
- Unordered
- compareSignaling - tiny modification of
compareQuiet
that can be added inextension
if 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 - magnitude copy copySign init(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:)
=quantized
let 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:
pow
withInt
argument - I want to write this myself.- PeRfOrMaNcE - especially any of the
@inlinable/usableFromInline
things. Just don't.
- 2-space indents and no tabs at all
- 80 characters per line
- Required
self
in methods and computed properties- All of the other method arguments are named, so we will require it for this one.
Self
/type name
for static methods is recommended, but not required.- I’m sure that they will depreciate the implicit
self
in 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
if
condition.- You can use a single logical operator - something like
if !isPrincess
orif isDisnepCharacter && isPrincess
is 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.