I am building some classes and code that store and perform arithmetic on currency values. I was originally using Doubles, but converted to Decimal due to arithmetic errors.
I am trying to find the best way to run unit tests against functions working with Decimal type.
Consider position.totalCost is a Decimal type.
XCTAssertEqual(position.totalCost, 3571.6, accuracy: 0.01)
This code does not compile because Decimal does not conform to FloatingPoint. XCTAssertEqual requires parameters to be Doubles or Floats.
I got around this by doing the following:
XCTAssertTrue(position.totalCost == 3571.6)
Which does work, but if an error arises during the unit test, I get a vague message:
XCTAssertTrue failed rather than the more useful XCTAssertEqual failed: ("2.0") is not equal to ("1.0")
So using XCTAssertEqual is ideal.
Potential Options (as a novice, no clue which is better or viable)
Code my Position class to store all properties as Decimal but use computed properties to get and set them as Doubles.
Write a custom assertion that accepts Decimals. This is probably the most 'proper' path because the only issue I've encountered so far with using Decimals is that XCT assertions cannot accept them.
Write a goofy Decimal extension that will return a Double value. For whatever reason, there is no property or function in the Decimal class that returns a Double or Floag.
Don't convert Decimal to a floating point if you don't have to since it will result in a loss of precision. If you want to compare two Decimal values with some accuracy you can use Decimal.distance(to:) function like so:
let other = Decimal(35716) / Decimal(10) // 3571.6
let absoluteDistance = abs(position.totalCost.distance(to: other))
let accuracy = Decimal(1) / Decimal(100) // 0.01
XCTAssertTrue(absoluteDistance < accuracy)
You can write an extension on Decimal:
extension Decimal {
func isEqual(to other: Decimal, accuracy: Decimal) -> Bool {
abs(distance(to: other)).isLess(than: accuracy)
}
}
And then use it in your tests:
XCTAssertTrue(position.totalCost.isEqual(to: 3571.6, accuracy: 0.01))
This is likely good enough. However, to get better error messages in the case of a failing test would require writing an overload for XCTAssertEqual, which is actually a bit tricky because elements of XCTest are not publicly available.
However, it is possible to approximate the behaviour:
Firstly, we need some plumbing to evaluate assertions, this can more or less be lifted straight from swift-corelibs-xctest.
import Foundation
import XCTest
internal enum __XCTAssertionResult {
case success
case expectedFailure(String?)
case unexpectedFailure(Swift.Error)
var isExpected: Bool {
switch self {
case .unexpectedFailure: return false
default: return true
}
}
func failureDescription() -> String {
let explanation: String
switch self {
case .success: explanation = "passed"
case .expectedFailure(let details?): explanation = "failed: \(details)"
case .expectedFailure: explanation = "failed"
case .unexpectedFailure(let error): explanation = "threw error \"\(error)\""
}
return explanation
}
}
internal func __XCTEvaluateAssertion(testCase: XCTestCase, _ message: #autoclosure () -> String = "", file: StaticString = #file, line: UInt = #line, expression: () throws -> __XCTAssertionResult) {
let result: __XCTAssertionResult
do {
result = try expression()
}
catch {
result = .unexpectedFailure(error)
}
switch result {
case .success: return
default:
let customMessage = message()
let description = customMessage.isEmpty ? result.failureDescription() : "\(result.failureDescription()) - \(customMessage)"
testCase.record(.init(
type: .assertionFailure,
compactDescription: description,
detailedDescription: nil,
sourceCodeContext: .init(
location: .init(filePath: String(describing: file), lineNumber: Int(line))
),
associatedError: nil,
attachments: [])
)
}
}
Now, for all of this to work, requires us to have access to the currently running XCTestCase, inside a global XCTAssert* function, which is not possible. Instead we can add our assert function in an extension.
extension XCTestCase {
func AssertEqual(
_ expression1: #autoclosure () throws -> Decimal,
_ expression2: #autoclosure () throws -> Decimal,
accuracy: #autoclosure () throws -> Decimal,
_ message: #autoclosure () -> String = "",
file: StaticString = #file,
line: UInt = #line
) {
__XCTEvaluateAssertion(testCase: self, message(), file: file, line: line) {
let lhs = try expression1()
let rhs = try expression2()
let acc = try accuracy()
guard lhs.isEqual(to: rhs, accuracy: acc) else {
return .expectedFailure("(\"\(lhs)\") is not equal to (\"\(rhs)\")")
}
return .success
}
}
}
All of which allows us to write our test cases as follows...
class MyTests: XCTestCase {
// etc
func test_decimal_equality() {
AssertEquals(position.totalCost, 3571.6, accuracy: 0.01)
}
}
And if the assertion fails, the test case will fail, with the message: ("3571.5") is not equal to ("3571.6") at the correct line.
We also cannot call our function XCTAssertEquals, as this will override all the global assert functions.
You milage may vary, but once you have the plumbing in place, this allows you to write bespoke custom assertions for your test suite.
Do you really need to specify the accuracy of 0.01?
Because if you omit this argument, it compiles just fine.
struct Position {
let totalCost: Decimal
}
let position = Position(totalCost: 3571.6)
//passes
XCTAssertEqual(position.totalCost, 3571.6)
// XCTAssertEqual failed: ("3571.6") is not equal to ("3571.61")
XCTAssertEqual(position.totalCost, 3571.61)
Related
The following swift program defines an enumeration called tempConverter with 2 cases - Fahrenheit & Celsius. That same enumeration also defines a method convert which takes a temperature value as a Double, and returns a C to F or F to C temperature conversion, depending on what the initialized value of tempConverter.self is. It also defines a method called switchScale which simply returns both Celsius & Fahrenheit conversions for the Double provided. When calling these methods & printing their results, I am getting a series of () characters outputted after the conversion values. Here's a link to a visual of the output I am seeing, reference this if you do not want to copy & run the code yourself.
Here's the code in its entirety, I really don't know why these () characters are being displayed, or how to remove them from the output.
Any help is appreciated!
(p.s) I know I spelt Fahrenheit wrong in the program, I have no excuse & am simply stupid. Carry on.
import Foundation
enum tempConverter{
case Celsius
case Farenheit
init(in scale: String){
self = .Celsius
scale.lowercased()
if scale.contains("c"){
self = .Celsius
}
else if scale.contains("f"){
self = .Farenheit
}
}
func convert(for temp: Double){
switch(self){
case .Celsius: print(temp, "c ->", (temp * 9)/5 + 32, "f")
break
case .Farenheit: print(temp, "f ->", (temp - 32) * 5 / 9, "c")
break
}
}
mutating func switchScale(){
switch(self){
case .Celsius:
self = .Farenheit
print(convert(for: temp))
break
case .Farenheit:
self = .Celsius
print(convert(for: temp))
break
}
}
}
var instance = tempConverter(in: "f")
var temp = 32.0
print(instance.convert(for: temp), instance.switchScale())
temp = 212.0
print(instance.convert(for: temp), instance.switchScale())
There is a lot going on with your code that I would change but to answer your specific question, the reason you are getting parentheses in your output is because you are printing a function.
struct SomeStruct {
func someFunc() {}
}
let s = SomeStruct()
print(s.someFunc()) // prints ()
You do that here:
print(instance.convert(for: temp)) // prints ()
What you see as output is that you are printing the returned value from the function call.
Functions that doesn't return a real value are said to return void and void can in swift be represented either with the keyword Void or by empty parentheses, ()
So in your case the function func convert(for temp: Double) returns void and this is printed as ()
You can actually declare the function to return void if you want to be extra clear (but you don't need to, the compiler understands you anyway) so
func convert(for temp: Double) { ... }
func convert(for temp: Double) -> Void { ... }
func convert(for temp: Double) -> () { ... }
all mean the same.
Off topic but maybe you mean for your function to return a Double so you could print that instead. Like this for example
func convert(for temp: Double) -> Double {
switch(self){
case .Celsius:
return temp * 9/5 + 32
case .Farenheit:
return (temp - 32) * 5 / 9
}
}
I have been at this for hours and can't work it out, please help. I am doing some coding practice to help sharpen up my skills in swift and this one seems so easy but I can't work it out.
I need to create a simple function that returns (the challenge i'm doing asks for this I haven't made it up) the sum of numbers as a string, but if the string contains characters, not numbers, it should return -1. It says : Receive two values of type string. Add them together. If an input is a character, return -1
This is where I am up to but i can't get it pass the tests for returning -1. It passes 3 / 5 tests where it's fine with numbers, but not with the characters. My thinking is that the character set line should check for if myNewString contains any of those characters it should return -1
func addStrNums(_ num1: String, _ num2: String) -> String {
// write your code here
var op1 = num1
var op2 = num2
var total: Int = 0
var myNewInt = Int(op1) ?? 0
var myNewInt2 = Int(op2) ?? 0
total = myNewInt + myNewInt2
var myNewString = String (total)
let characterset = CharacterSet(charactersIn:
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!#£$%^&*()_-=+;/?><"
)
if myNewString.rangeOfCharacter(from: characterset) != nil {
return "-1"
}
return myNewString
}
Results of above is :
Test Passed: 10 == 10
FAILED: Expected: -1, instead got: 5
Test Passed: 1 == 1
Test Passed: 3 == 3
import foundation
func addStringNumber(_ num1: String, num2: String) -> String {
if !CharacterSet.decimalDigits.isSuperset(of: CharacterSet(charactersIn: num1)) {
return "-1"
}
if !CharacterSet.decimalDigits.isSuperset(of: CharacterSet(charactersIn: num2)) {
return ""
}
let sum = String.init(format: "%d", Int((num1 as NSString).intValue + (num2 as NSString).intValue))
return sum
}
Note:- for this import foundation is important.
A couple of thoughts:
You are using nil coalescing operator (??), which effectively says that you want to ignore errors parsing the integers. I would suggest that you want to detect those errors. E.g.
func addStrNums(_ num1: String, _ num2: String) -> String {
guard
let value1 = Int(num1),
let value2 = Int(num2)
else {
return "-1"
}
let total = value1 + value2
return "\(total)"
}
The initializer for Int will automatically fail if there are non-numeric characters.
These programming tests generally don’t worry about localization concerns, but in a real app, we would almost never use this Int string-to-integer conversion (because we want to honor the localization settings of the user’s device). For this reason, we would generally prefer NumberFormatter in real apps for users with different locales:
let formatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
return formatter
}()
func addStrNums(_ num1: String, _ num2: String) -> String {
guard
let value1 = formatter.number(from: num1)?.intValue, // use `doubleValue` if you want to handle floating point numbers, too
let value2 = formatter.number(from: num2)?.intValue
else {
return "-1"
}
let total = value1 + value2
return formatter.string(for: total) ?? "-1"
}
This accepts input that includes thousands separators and formats the output accordingly. (Obviously, feel free to use whatever numberStyle you want.)
Also, be aware that NumberFormatter is more tolerant of whitespace before or after the digits. It also allows for a wider array of numeric characters, e.g. it recognizes “5”, a digit entered with Japanese keyboard, which is different than the standard ASCII “5”. This greater flexibility is important in a real app, but I don’t know what your programming exercise is requiring.
While the above demonstrates that you don’t have to check for non-numeric digits manually, you can if you need to. But you need to check the two input strings, not the string representation of the total (especially if you used nil coalescing operator to disregard errors when converting the strings to integers, as I discussed in point 1 above).
If you do this, though, I would not advise trying list all of the non-numeric characters yourself. Instead, use the inverted set:
let characterSet = CharacterSet(charactersIn: "0123456789").inverted // check for 0-9 if using `Int` To convert, use `CharacterSet.decimalDigits` if converting with `NumberFormatter`
guard
num1.rangeOfCharacter(from: characterSet) == nil,
num2.rangeOfCharacter(from: characterSet) == nil
else {
return "-1"
}
Or, one could use regex to confirm that there are only one or more digits (\d+) between the start of the string (^) and the end of the string ($):
guard
num1.range(of: #"^\d+$"#, options: .regularExpression) != nil, // or `#"^-?\d+$"#` if you want to accept negative values, too
num2.range(of: #"^\d+$"#, options: .regularExpression) != nil
else {
return "-1"
}
FWIW, if you’re not familiar with it, the #"..."# syntax employs “extended string delimiters” (saving us from having to to escape the \ characters within the string).
As an aside, you mentioned a command line app that should return -1. Generally when we talk about command line apps returning values, we’re exiting with a numeric value not a string. I would be inclined to make this function return an Int?, i.e. either the numeric sum or nil on failure. But without seeing the particulars on your coding test, it is hard to be more specific.
This is what I've tried and can't figure out where the error is coming from. Is there something missing? Syntax error? I tried doing similar with if-else in the function and also getting errors.
var steps = 0
func incrementSteps() -> Int {
steps += 1
print(steps)
return steps
}
incrementSteps()
let goal = 10000
func progressUpdate() -> Int {
let updated = progressUpdate()/goal
switch updated {
case (0.0..<0.1):
print("Great start")
case (0.11..<0.5):
print("Almost halfway")
case (0.51..<0.9):
print("Almost complete")
default:
print("Beat goal")
}
}
progressUpdate()
You need to specify updated as Double. And cast it back to Int when returning(if you require Int for your requirement).
Note: Also, you need to modify calling the progressUpdate function within progressUpdate definition which causes a recursion. If you want to do so you might want to give condition to break the loop.
func progressUpdate() -> Int {
let updated = Double(steps/goal)
switch updated {
case (0.0..<0.1):
print("Great start")
case (0.11..<0.5):
print("Almost halfway")
case (0.51..<0.9):
print("Almost complete")
default:
print("Beat goal")
}
return Int(updated)
}
The following works in Playground:
func stringToInt(numberStr: String!) -> Int {
print(numberStr)
return Int(numberStr)!
}
let strNum1: String?
strNum1 = "1"
let result = stringToInt(numberStr: strNum1)
It returns 1 as expected.
In Xcode, a similar approach fails:
func stringToInt(numberStr: String!) -> Int {
print("\(numberStr!)")
let str = "\(numberStr!)"
print(Int(str))
return Int(str)!
}
The first print produces: Optional(1)
The second print produces: nil
The return statement fails because it is attempting to create an Int from a nil.
It must be something simple but I haven't been able to determine why it's not working. This is in Swift 3 and Xcode 8 BTW.
#Hamish:
In Xcode, I have a string with a numeric value. This:
print("number: (selectedAlertNumber) - unit: (selectedAlertUnit)")
...produces this:
number: Optional(1) - unit: Day
Then, I'm checking to see if either selectedAlertNumber of selecterAlertUnit != "-"
if selectedAlertNumber != "-" && selectedAlertUnit != "-" {
// set alert text
var unitStr = selectedAlertUnit
let alertNumber = stringToInt(numberStr: selectedAlertNumber)
if alertNumber > 1 {
unitStr.append("s")
}
let alertText = "...\(selectedAlertNumber) \(unitStr) before event."
alertTimeCell.setAlertText(alertText: alertText)
// set alert date/time
}
The let alertNumber = stringToInt... line is how I'm calling the function. I could just attempt the conversion there but I wanted to isolate the problem by wrapping the conversion in it's own function.
Using string interpolation to convert values to a String is usually not advised since the output may differ depending on optional status of the value. For example, consider these two functions:
func stringToInt(numberStr: String!) -> Int
{
print("\(numberStr!)")
let str = "\(numberStr!)"
return Int(str)!
}
func otherStringToInt(numberStr: String!) -> Int
{
print(numberStr)
let str = "\(numberStr)"
return Int(str)!
}
The only difference between these two is the ! in the second function when using string interpolation to get a String type value from numberStr. To be more specific, at the same line in function 1 compared to function 2, the string values are very different depending on whether or not the interpolated value is optional:
let str1: String = "1"
let str2: String! = "1"
let str3: String? = "1"
let otherStr1 = "\(str1)" // value: "1"
let otherStr2 = "\(str2)" // value: "Optional(1)"
let otherStr3 = "\(str2!)" // value: "1"
let otherStr4 = "\(str3)" // value: "Optional(1)"
let otherStr5 = "\(str3!)" // value: "1"
Passing otherStr2 or otherStr4 into the Int initializer will produce nil, since the string "Optional(1)" is not convertible to Int. Additionally, this will cause an error during the force unwrap. Instead of using string interpolation in your function, it would be better to just use the value directly since it's already a String.
func stringToInt(numberStr: String!) -> Int
{
return Int(numberStr)!
}
Let me know if this makes sense.
Also, my own personal feedback: watch out force unwrapping so frequently. In many cases, you're running the risk of getting an error while unwrapping a nil optional.
I have a dictionary of formulas (in closures) that I now what to use in a function to calculate some results.
var formulas: [String: (Double, Double) -> Double] = [
"Epley": {(weightLifted, repetitions) -> Double in return weightLifted * (1 + (repetitions)/30)},
"Brzychi": {(weightLifted, repetitions) -> Double in return weightLifted * (36/(37 - repetitions)) }]
Now I'm trying to write a function that will get the correct formula from the dictionary based on the name, calculate the result, and return it.
func calculateOneRepMax(weightLifted: Double, repetitions: Double) -> Double {
if let oneRepMax = formulas["Epley"] { $0, $1 } <-- Errors here because I clearly don't know how to do this part
return oneRepMax
}
var weightlifted = 160
var repetitions = 2
let oneRepMax = Calculator.calculateOneRepMax(weightlifted, repetitions)
Now Xcode is giving me errors like 'Consecutive statements on a line must be separated by a ';' which tells me the syntax I'm trying to use isn't correct.
On a side note, I wasn't sure if I should use a dictionary for this but after a lot of homework I'm confident it's the correct choice considering I need to iterate through it to get the values when I need them and I need to know the number of key/value pairs so I can do things like display their names in a Table View.
I've searched far and wide for answers, read Apple's documentation over and over and I'm really stuck.
Thanks
formulas["Epley"] returns an optional closure which needs to be
unwrapped before you can apply it to the given numbers. There are several options you can choose from:
Optional binding with if let:
func calculateOneRepMax(weightLifted: Double, repetitions: Double) -> Double {
if let formula = formulas["Epley"] {
return formula(weightLifted, repetitions)
} else {
return 0.0 // Some appropriate default value
}
}
This can be shortened with optional chaining and the
nil-coalescing operator ??:
func calculateOneRepMax(weightLifted: Double, repetitions: Double) -> Double {
return formulas["Epley"]?(weightLifted, repetitions) ?? 0.0
}
If a non-existing key should be treated as a fatal error instead
of returning a default value, then guard let would be
appropriate:
func calculateOneRepMax(weightLifted: Double, repetitions: Double) -> Double {
guard let formula = formulas["Epley"] else {
fatalError("Formula not found in dictionary")
}
return formula(weightLifted, repetitions)
}