Skip to content

Implement Decimal #659

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

Merged
merged 4 commits into from
Oct 13, 2016
Merged

Implement Decimal #659

merged 4 commits into from
Oct 13, 2016

Conversation

alblue
Copy link
Contributor

@alblue alblue commented Sep 27, 2016

Although not yet fully covered by tests, this is a nearly complete implementation of NSDecimal. There are cases where dealing with large and small numbers will overflow that isn't implemented yet (there is one remaining NSUnimplemented) but it can be used for simple mathematical operations.


public init(_exponent: Int32, _length: UInt32, _isNegative: UInt32, _isCompact: UInt32, _reserved: UInt32, _mantissa: (UInt16, UInt16, UInt16, UInt16, UInt16, UInt16, UInt16, UInt16)){
self._mantissa = _mantissa
self.__exponent = Int8(_exponent & Int32(0xff))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is a bug with this, in that negative exponents don't work as expected. It doesn't dial the tests but there needs some additional work before this goes through, as well as more tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in subsequent push

Copy link
Contributor Author

@alblue alblue left a comment

Choose a reason for hiding this comment

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

Changes are needed in the initialiser to handle negative exponents properly.

@alblue
Copy link
Contributor Author

alblue commented Sep 27, 2016

It looks like Int8(truncatingBitPattern:) is required here

public init(sign: FloatingPointSign, exponent: Int, significand: Decimal) {
self.init(_exponent: Int32(exponent) + significand._exponent, _length: significand._length, _isNegative: sign == .plus ? 0 : 1, _isCompact: significand._isCompact, _reserved: 0, _mantissa: significand._mantissa)
}
public init(signOf: Decimal, magnitudeOf magnitude: Decimal) { self.init(_exponent: magnitude._exponent, _length: magnitude._length, _isNegative: signOf._isNegative, _isCompact: magnitude._isCompact, _reserved: 0, _mantissa: magnitude._mantissa)
Copy link
Contributor

@xwu xwu Sep 29, 2016

Choose a reason for hiding this comment

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

Nit: missing newline before self.init

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can you explain what you mean? Self.init is on line 143 and the public init is on line 142.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, sorry - the comment was on line 145.

}

public func NSDecimalIsNotANumber(_ dcm: UnsafePointer<Decimal>) -> Bool { NSUnimplemented() }

public func NSDecimalIsNotANumber(_ dcm: UnsafePointer<Decimal>) -> Bool { return dcm.pointee.isNaN
Copy link
Contributor

@xwu xwu Sep 29, 2016

Choose a reason for hiding this comment

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

Nit: missing new line before return

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fair enough, can fix

_isCompact = 1
} else {
_isCompact = 0
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: could this be more terse? _isCompact = newValue ? 1 : 0

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, it could be but I don't know if there's aversion to using a ternary operator.

// Divide by 10 as much as possible
// Put the non-empty remainder in place
// Set the new exponent
isCompact = true
Copy link
Contributor

@xwu xwu Sep 29, 2016

Choose a reason for hiding this comment

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

I'm confused by this stub. It doesn't do anything, so why not leave NSDecimalCompact(_:) unimplemented?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Mainly because the NSDecimalCompact call is invoked from the init(Double) constructor, without which you couldn't test this. However that part of it is still a work in progress, so this could be left out of this patch set for now.

UInt8(UInt32(newValue & (0b11 << 16)) >> 10)
__reserved = UInt16(newValue & 0b1111_1111_1111_1111)
}
}
Copy link
Contributor

@phausler phausler Sep 29, 2016

Choose a reason for hiding this comment

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

these accessors change the structural layout and size of Decimal, it will probably make it incompatible when archiving and serializing it. Is there a compelling reason to re-layout the structure? To port addition and division and compaction it might be nice to be able to send these over to C functions.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

These bring the structure to be the same format as the Objective-C structure. At the moment both Linux and Objective-C are different. Secondly, there is a difference in behaviour visible, in that the results are truncated on Darwin but not on Linux (hence the tests for isCompact:3). That's because the structure on the Objective-C is packed. Note that I have run these tests on both Linux and Objective-C to verify the same behaviour. Trivially, on Darwin at the moment:

1> import Foundation
2> MemoryLayout.size
$R0: Int = 20

and on Linux:

1> import Foundation
2> MemoryLayout.size
$R0: Int = 36

I would argue that to be performant we would want to have these as the same data structure.

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm i guess the importer is claiming them to be different sizes than the actual bit-packed size. So i guess your solution is probably the best we can do for now sadly.

}
public static func ==(lhs: Decimal, rhs: Decimal) -> Bool {
if lhs.isNaN || rhs.isNaN {
return false
Copy link
Contributor

@phausler phausler Sep 29, 2016

Choose a reason for hiding this comment

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

To match the behavior of the objective-c version:

    // NaN is a special case and is arbitrary ordered before everything else
    // Conceptually comparing with NaN is bogus anyway but raising or 
    // always returning the same answer will confuse the sorting algorithms
   if(NSDecimalIsNotANumber(leftOperand)) {
        if(NSDecimalIsNotANumber(rightOperand)) {
            return NSOrderedSame;
        }
        return NSOrderedAscending;
    }

so if both are NaN then they are ordered same which is considered equal (irrespective of any other behavior standard of floating point numbers)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK, can fix that

Copy link
Contributor Author

Choose a reason for hiding this comment

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

By the way there's some inconsistency going on here because of this. On Darwin:

1> import Foundation
2> let a = Decimal.nan
3> let b = Decimal.nan
4> a.isEqual(to:b)
$R0: Bool = true
5> a.isTotallyOrdered(belowOrEqualTo: b)
$R1: Bool = false

You also have the fact that:

6> Double.nan == Double.nan
$R2: Bool = false

which makes it doubly confusing, though slightly off-topic for here.

I'm happy to implement/make this into bug compatible with Darwin but arguably the fact that you have a different behaviour between 'isEqual' and 'isTotallyOrdered' is a little strange.

if lhs.isNaN || rhs.isNaN {
return false
}
if lhs.__exponent == rhs.__exponent && lhs.__lengthAndFlags == rhs.__lengthAndFlags && lhs.__reserved == rhs.__reserved {
Copy link
Contributor

Choose a reason for hiding this comment

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

negative values fall into here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The 'lengthAndFlags' field contains both the length (upper nibble) and the compact/negative flags. So this will test for negative as well. We could explicitly test the flags individually but then we're doing a lot of byte wrapping for no real reason. The 'lengthAndFlags' looks like:

llllxxxx
xxxxCxxx
xxxxxNxx

Copy link
Contributor

Choose a reason for hiding this comment

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

got it; perhaps worth either a comment or a ivar name change to reflect that it it stored there

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Any suggestions as to what to call it? __lengthAndNegativeAndCompact?

}
var lhsVal = lhs
var rhsVal = rhs
return NSDecimalCompare(&lhsVal, &rhsVal) == .orderedSame
Copy link
Contributor

Choose a reason for hiding this comment

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

The Darwin version just calls this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, I don't understand the comment on its own. Is it related to other comments or code paths?

destination.pointee.__lengthAndFlags = source.pointee.__lengthAndFlags
destination.pointee.__exponent = source.pointee.__exponent
destination.pointee.__reserved = source.pointee.__reserved
destination.pointee._mantissa = source.pointee._mantissa
Copy link
Contributor

Choose a reason for hiding this comment

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

The objective-c version does not preserve reserved (but perhaps it should)

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'm happy to just leave setting reserved to a no-op and treating them as zero if you want me to change the Swift version

Copy link
Contributor

Choose a reason for hiding this comment

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

The only part that might matter is for verification of behavior. To API consumers it ought to not matter.


public func NSDecimalCompact(_ number: UnsafeMutablePointer<Decimal>) { NSUnimplemented() }
public func NSDecimalCompact(_ number: UnsafeMutablePointer<Decimal>) {
number.pointee.compact()
Copy link
Contributor

Choose a reason for hiding this comment

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

This is upside down from the objective-c implementation, for consistency sake we might want to put the workhorse logic here instead of member functions (and perhaps treat the NSDecimal* functions as "implementation details"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Using the UnsafeMutablePointer is actually pretty painful. You can't (for example) have 'var d = number.pointee; d.doStuff()' because it ends up with a different copy. So everywhere you have the functions you need to do number.pointee to use the passed in version. In addition, it's not being safe for the sake of it, but rather adopting an older API.

Finally you can't tell what is being mutated and what isn't with these APIs; you're just passing an opaque pointer. This is tedious when doing non-mutating code (e.g. the NSDecimalIsNotANumber) because you have can't pass in a 'let' constant:

1> import Foundation
2> let one = Decimal(1)
one: Decimal = {
_mantissa = {
0 = 8448
1 = 0
2 = 1
3 = 0
4 = 0
5 = 0
6 = 0
7 = 0
}
}
3> NSDecimalIsNotANumber(one)
error: repl.swift:3:23: error: cannot convert value of type 'Decimal' to expected argument type 'UnsafePointer'
NSDecimalIsNotANumber(one)
^~~

3> NSDecimalIsNotANumber(&one)
error: repl.swift:3:23: error: cannot pass immutable value as inout argument: 'one' is a 'let' constant
NSDecimalIsNotANumber(&one)
^~~~

Having those as the API is probably a bad thing to do; in fact, I'd argue that the NSDecimalXxxx functions should be marked as deprecated. Right now the simple (non-mutating) ones can be more efficiently implemented (read: not taking an unsafe pointer) by doing it the reverse way from the way that Objective-C did it, and having the functions as backwards compatible for those that need it.

Copy link
Contributor

Choose a reason for hiding this comment

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

We can't mark them as deprecated and still use them to power the extended methods on NSDecimal (perhaps we could rename them so that they are not publicly autocompleted; but that is not going to happen likely for Swift 3.X).

Often when making the overlay I had to use a temp copy to transact upon instead. And we can't make them hoisted since they transact upon an address of a Decimal not the Decimal itself.

The Decimal type on Darwin is a packed structure, and so the
implementation in Swift needs to follow the same packed layout
to ensure that the values are truncated in the same way.

Add test coverage for initialisers and verify the same test
code passes on Swift on Darwin with the same results.
This adds implementations and test coverage of Decimal
which passes on both Darwin and Linux.
Provides implementations of multiply and power, as well as
a higher-precision Decimal equivalent that is used for implementing
mathematical operations internally.
@alblue alblue changed the title Add initialisers and test coverage for Decimal Implement Decimal Oct 13, 2016
@alblue
Copy link
Contributor Author

alblue commented Oct 13, 2016

This isn't yet complete (test coverage and normalisation NSUnimplemented still to go) but is in a state where the content can be seriously reviewed ahead of merging.

return error;
}

for i in 0..<Decimal.maxSize {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This should probably be big._length rather than Decimal.maxSize, otherwise we'll lose precision

//
var backup = Decimal()

NSDecimalCopy(&backup,aa)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

NB this seems redundant at the moment but will be used in the NSUnimplemented below

result.pointee._exponent = newExponent
}
result.pointee._length = big._length
for i in 0..<Decimal.maxSize {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This should be big._length as well, I think

newExponent += exponent
}

for i in 0..<Decimal.maxSize { // bigSize?
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Think this should be big._length as well

var isZero:Bool { get }
}

extension Decimal: VariableLengthNumber {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure if we can apply fileprivate here but we should if we could

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nope, compile time error to try. Still, VariableLengthNumber is an internal protocol so hopefully it won't be visible.


public func NSDecimalMultiplyByPowerOf10(_ result: UnsafeMutablePointer<Decimal>, _ number: UnsafePointer<Decimal>, _ power: Int16, _ roundingMode: NSDecimalNumber.RoundingMode) -> NSDecimalNumber.CalculationError { NSUnimplemented() }
// == Internal (Swifty) functions ==
Copy link
Contributor Author

Choose a reason for hiding this comment

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

It would be good to make these public API and have the same on the Darwin SDK - thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

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

That would have to be something that we would need to pass through API review and decide on if they have the merit to maintain etc. Can they be re-written to use the free floating functions? it seems like there is some duplication of effort here.

Ideally we should have these to be sharable code from swift-corelibs-foundation to the Darwin overlay for Foundation.

The integer division requires the creation of a new protocol
to unify WideDecimal and Decimal. This has been referred to as
VariableLengthNumber, because it builds upon the functions in
CoreFoundation that used NSInteger built-in functions, but without
conflating the Swift Integer type.
}

public func NSDecimalIsNotANumber(_ dcm: UnsafePointer<Decimal>) -> Bool { NSUnimplemented() }
fileprivate func divideByShort<T:VariableLengthNumber>(_ d: inout T, _ divisor:UInt16) -> (UInt16,NSDecimalNumber.CalculationError) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Divide and carry, multiply and carry, etc, are best off using the new double-width divide and multiply facilities now available with new integer protocols. Those were exposed precisely for implementation of types such as Decimal.

Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't these be internal methods on Decimal, not free functions?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

These were based on the implementation from Core Foundation for ease of review and acceptance. I am fine with the implementations being modified subsequently to use alternative methods of calculation. I would caution, though, that the NSDecimal* top level functions are public API and are a real nightmare to implement as sane methods on Decimal, so it is not trivial to do as such.

Copy link
Contributor

Choose a reason for hiding this comment

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

When we added functionality to Decimal to have methods it became quite clear that it is nearly impossible to mimic the behavior and avoid needing to call out to the freestanding functions. They will most likely need to remain API in swift to service the hoisted method implementations. The divideByShort and fellow methods are relatively complex and hold some quirky behavior that these seem to attempt to mimic - we should consider porting some of the edge case tests from Foundation to swift-corelibs

}

fileprivate extension UInt16 {
func compareTo(_ other: UInt16) -> ComparisonResult {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do you need this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

To make it easier to implement the code

Copy link
Contributor

Choose a reason for hiding this comment

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

It looks like it's used only once and can be trivially inlined. Doing so would have performance benefit and improve clarity at the use site.

// This formula and test for q gives at most q+1; See Knuth for proof.

let ul = u._length
let uu = u // work around compiler bug
Copy link
Contributor

Choose a reason for hiding this comment

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

What compiler bug is this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

"Cannot subscript inout parameter u of type Decimal"

Copy link
Contributor

Choose a reason for hiding this comment

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

might be good to attach bugs.swift.org url here for a filed ticket just so it is known what bug it is

Copy link
Contributor

Choose a reason for hiding this comment

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

On what line are you encountering this error? You are clearly subscripting u successfully elsewhere, so something must be different about a particular use site where the argument is inout. Can you point that out and file a bug?

* individual operands, but often produces a more
* accurate result later. I chose 19 arbitrarily
* as half of the magic 38, so that normalization
* doesn't always occur. */
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems incorrect. What is the behavior of Darwin Foundation?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the comment from Darwin Foundation.

if r._length != 0 {
r.negate()
}
return NSDecimalAdd(result, leftOperand, &r, roundingMode)
Copy link
Contributor

Choose a reason for hiding this comment

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

If you negate the operand, then the rounding mode may change.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The implementation is based on Foundation

Copy link
Contributor

@phausler phausler left a comment

Choose a reason for hiding this comment

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

Overall this looks pretty good.

return .noError;
}

NSUnimplemented() // work in progress
Copy link
Contributor

Choose a reason for hiding this comment

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

might be good to comment on the remaining work here that needs to be done as a reminder ;)


public func NSDecimalMultiplyByPowerOf10(_ result: UnsafeMutablePointer<Decimal>, _ number: UnsafePointer<Decimal>, _ power: Int16, _ roundingMode: NSDecimalNumber.RoundingMode) -> NSDecimalNumber.CalculationError { NSUnimplemented() }
// == Internal (Swifty) functions ==
Copy link
Contributor

Choose a reason for hiding this comment

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

That would have to be something that we would need to pass through API review and decide on if they have the merit to maintain etc. Can they be re-written to use the free floating functions? it seems like there is some duplication of effort here.

Ideally we should have these to be sharable code from swift-corelibs-foundation to the Darwin overlay for Foundation.

@phausler
Copy link
Contributor

@swift-ci Please test and merge

@swift-ci swift-ci merged commit cafc428 into swiftlang:master Oct 13, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants