Leonardo Maia Pugliese
Holy Swift

Holy Swift

Using native Swift to make Units Conversions

Using native Swift to make Units Conversions

A Cryptocurrency example!

Leonardo Maia Pugliese's photo
Leonardo Maia Pugliese
·Jan 25, 2022·

8 min read

Subscribe to my newsletter and never miss my upcoming articles

Hallo mijn vrienden, Leo Hier.

Today we will explore something that I learn two weeks ago.

I was wondering if there's a native way to convert hours to seconds or even kilometers to meters. The good thing is that Apple already thought that for us and we don't need to re-implement those functions.

You will learn how to use the Measurement generic struct, and how this can be handy in your day-to-day life. Unfortunately, it is only for iOS 10 and above.

Disclaimer: You should keep in mind that as a Foundation API, you don't have control over it. So if you need very strict control of the API's behavior you should do it yourself. Of course, you can decrease the Apple factor risk covering your APIs with unit tests, so if something default changes, you are prepared and the production code will not explode.

Let's code! But first...

The Painting

Today's painting is called Woman Pouring Water into a Jar is a 1640 piece from Gerrit Dou. Gerrit Dou (7 April 1613 – 9 February 1675), also known as Gerard and Douw or Dow, was a Dutch Golden Age painter, whose small, highly polished paintings are typical of the Leiden fijnschilders. He specialized in genre scenes and is noted for his trompe l'oeil "niche" paintings and candlelit night scenes with strong chiaroscuro. He was a student of Rembrandt.

From Rembrandt, he acquired his skill in coloring and the more subtle effects of chiaroscuro, and his master's style is reflected in several of his earlier pictures.

I chose this picture because with the knowledge within this post you can create a converter that can convert a jar of water into a bowl of water, and this painting has a jar of water that can be converted into a bowl of water. Got it?

The Problem

You need to make a unit conversion process for a cryptocurrency module and you don't want to make an in-house solution for that.

You can always try to find a cocoapod that does that for you. But that's not what we intended here. I wish that we have a native Swift way to unit conversion. And I gladly found this: the Measurement struct. It is a generic struct part of the Foundation framework, so it's guaranteed you already have it in your codebase.

Let's deep dive into it and see the capabilities.

Measurement Definition

The overview from Apple is that object represents a quantity and unit of measure. Both concepts are very important when you are dealing with unit conversion because one says what is and the other say about how much of that we have.

Additionally Measurement objects provide a programmatic interface to converting measurements into different units, as well as calculating the sum or difference between two measurements. Measurements support a large set of operators, including +, -, *, /, and a full set of comparison operators. All those traits make this API very user-friendly and desired against third-party or in-house solutions since we have it for free with Foundation.

The way to initialize is using a Unit object and double value. And the last thing is that Measurement objects are immutable, and cannot be changed after being created, which is good because having to manage states between conversions it's not what we could call "handy".

Implementation

Remember that we said earlier you should start your measurement with a type and a quantity? The example is here:

var seconds = Measurement<UnitDuration>(value: 7200, unit: .seconds )
print("Unit before conversion -> ", seconds)

print()

let hours = seconds.converted(to: .hours)
print("Unit after conversion -> ", hours)
print("Measurement value -> ", hours.value)
print("Measurement unit symbol ->", hours.unit.symbol)

Resulting in:

Screenshot 2022-01-24 at 08.36.00.png

Before further API exploration, we'll deep dive into that UnitDuration type and how it can do all of the conversions for us.

Deep dive into Measurement

You must be asking yourself: "But how the Measurement knows how to convert all units with only that API???". Its because Measurement is leveraged by the power of generics.

When you are writing code that has the same structure and the same functions just changing the meaning of those structures, you have in your hands a great candidate for the use of Generics.

Looking for the Measurement implementation is defined as:

public struct Measurement<UnitType> : ReferenceConvertible, Comparable, Equatable where UnitType : Unit {

    public typealias ReferenceType = NSMeasurement

    public let unit: UnitType
    public var value: Double

    public init(value: Double, unit: UnitType)

    public func hash(into hasher: inout Hasher)
    public var hashValue: Int { get }
}

Look at both of the attributes that every measurement has, the unit and the value, in other words, the "what" and "how many". Also here we the UnitType appearance that is a Unit. But wait... what is a Unit? Each instance of a Unit subclass consists of a symbol, which can be used to create string representations of NSMeasurement objects with the MeasurementFormatter class.

Let's see:

@available(iOS 10.0, *)
open class Unit : NSObject, NSCopying, NSSecureCoding {

    open var symbol: String { get }
    public init(symbol: String)

}

Good, now we know every the symbol or the Unit comes from. But now... where's the convert function come from? Well, it is a type specialization of Measurement. Check below:

@available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *)
extension Measurement where UnitType : Dimension {

    public func converted(to otherUnit: UnitType) -> Measurement<UnitType>
    public mutating func convert(to otherUnit: UnitType)
    public static func - (lhs: Measurement<UnitType>, rhs: Measurement<UnitType>) -> Measurement<UnitType>
    public static func + (lhs: Measurement<UnitType>, rhs: Measurement<UnitType>) -> Measurement<UnitType>

}

So the Measurement type will have the convert andconvertedfunctions. Theconvertfunction is done in place, this means you mutate the unit and the value of theMeasurementand the converted you create a newMeasurement` type with your converted type. You should use what suits you better.

Wait a minute, what is the Dimension type? And how does the measurement type know how to convert every generic type of Unit?

Dimension is not the Unit or it is? As you are already guessing it must be a type of Unit. And now we came to the last piece of our puzzle, it's a generic way to convert all the units with one structure, beautiful. Check it out:

open class Dimension : Unit, NSSecureCoding {

    @NSCopying open var converter: UnitConverter { get }

    public init(symbol: String, converter: UnitConverter)

    open class func baseUnit() -> Self
}

All the Dimension objects have a UnitConverter object within. That is responsible to do all the conversions! But how it does do the conversions? The UnitConverter is the answer:

@available(iOS 10.0, *)
open class UnitConverter : NSObject {
    open func baseUnitValue(fromValue value: Double) -> Double

    open func value(fromBaseUnitValue baseUnitValue: Double) -> Double
}

Each instance of a Dimension subclass has a converter, which represents the unit in terms of the dimension’s baseUnit(). For example, the NSLengthUnit class uses meters as its base unit. The system defines the predefined miles unit by a UnitConverterLinear with a coefficient of 1609.34, which corresponds to the conversion ratio of miles to meters (1 mi = 1609.34 m); the system defines the predefined meters unit by a UnitConverterLinear with a coefficient of 1.0 because it’s the base unit.

The final puzzle is: You create a Measurement object that has a quantity and a UnitType, if that unit type is a Dimension it enables you to convert any unit within that UnitType.

Unit conversion Types

The Foundation Framework brings a lot out of the box Dimension objects to play with they are, but not limited to:

  1. UnitAcceleration - With metersPerSecondSquared and gravity attributes
  2. UnitAngle - That has degrees, radians etc properties.
  3. UnitArea - That has squareMeters, acres, squareInches etc properties.
  4. UnitConcentrationMass - That has gramsPerLiter, milligramsPerDeciliter properties
  5. UnitDuration - That has hours, minutes, secods etc

And many more! Check the docs to see them all.

Create your Custom Units

Finally, let's answer the question from the beginning and create our Cryptocurrency Converter. Let's say you are going to provide only three cryptocurrencies: bitcoin, ethereum, and Tron. (This is just an example, not an investment recommendation).

The only thing you have to do is to implement the Dimension protocol and override the baseUnit function and you are good to go! Copy/paste the code below in your playgrounds:

final class CryptoCurrency: Dimension {
    static let bitcoin = CryptoCurrency(symbol: "BTC",
                                            converter: UnitConverterLinear(coefficient: 1.0))
    static let ethereum = CryptoCurrency(symbol: "ETH",
                                             converter: UnitConverterLinear(coefficient: 1/15.22))
    static let tron = CryptoCurrency(symbol: "TRX",
                                         converter: UnitConverterLinear(coefficient: 1/659943.0))

    override class func baseUnit() -> CryptoCurrency {
        return bitcoin
    }
}

Here we are setting up the three supported currencies. To create a new CryptoCurrency you just need to provide the symbol and the converter. In our case, everything will be indexed to bitcoin, and it's inversion proportional so the ratio is one divided by the proportion. In the real world, this coefficient would change so you would have more logic on it, but you get the point.

To test let's create some bitcoins and convert them:

let bitcoins = Measurement<CryptoCurrency>(value: 3, unit: .bitcoin)
print("Bitcoins before conversion -> ", bitcoins)

print()
let ethereum = bitcoins.converted(to: .ethereum)
print("Ethereum Unit after conversion -> ", ethereum)
print("Ethereum Measurement value -> ", ethereum.value)
print("Ethereum Measurement unit symbol ->", ethereum.unit.symbol)

print()
let tron = bitcoins.converted(to: .tron)
print("Tron Unit after conversion -> ", tron)
print("Tron Measurement value -> ", ethereum.value)
print("Tron Measurement unit symbol ->", ethereum.unit.symbol)

Resulting in:

Screenshot 2022-01-25 at 08.21.03.png

And we are done!

Summary

Today we studied the Measurement API and how it can be flexible to your needs. You can use the default Dimensions and Units as well as build your own Dimension to accomplish your tasks!

That's all my people, I hope you liked it as I enjoyed writing this article. If you want to support this blog you can Buy Me a Coffee or just leave a comment saying hello. You can also sponsor posts and I'm open to writing freelancing! Just reach me in LinkedIn or Twitter for details.

Thanks for the reading and... That's all folks.

credits: image

Did you find this article valuable?

Support Leonardo Maia Pugliese by becoming a sponsor. Any amount is appreciated!

Learn more about Hashnode Sponsors
 
Share this