Trying to extend IntegerType (and FloatingPointType); Why can't all Int types be converted to NSTimeInterval

(This probably needs a better title…)

I would like to have a set of accessors I can use in code to quickly express time durations. E.g:

  • Swift 2.1 --utf8--> String?
  • Checking if text fields are empty cause error in Swift 2
  • How to get IPhone's Public IP Address without using a third party url
  • Display data in two different TableView in the same View in Swift
  • Down casting multiple protocol Array<protocol<P1, P2>> to Array<P1>
  • Custom Font in IOS Not Reflected On Device
  • 42.seconds
    3.14.minutes
    0.5.hours
    13.days
    

    This post illustrates that you can’t just do it with a simple new protocol, extension, and forcing IntegerType and FloatingPointType to adopt that. So I thought I’d just go the more redundant route and just extend IntegerType directly (and then repeat the code for FloatingPointType).

    extension IntegerType {
        var seconds:NSTimeInterval {
            return NSTimeInterval(self)
        }
    }
    

    The error generated is confusing:

    Playground execution failed: /var/folders/2k/6y8rslzn1m95gjpg534j7v8jzr03tz/T/./lldb/9325/playground131.swift:31:10: error: cannot invoke initializer for type 'NSTimeInterval' with an argument list of type '(Self)'
                    return NSTimeInterval(self)
                           ^
    /var/folders/2k/6y8rslzn1m95gjpg534j7v8jzr03tz/T/./lldb/9325/playground131.swift:31:10: note: overloads for 'NSTimeInterval' exist with these partially matching parameter lists: (Double), (UInt8), (Int8), (UInt16), (Int16), (UInt32), (Int32), (UInt64), (Int64), (UInt), (Int), (Float), (Float80), (String), (CGFloat), (NSNumber)
                    return NSTimeInterval(self)
    

    What confuses me is that it seems to say that I can’t do an NSTimeInterval() initializer with (Self), but everything Self represents is listed in the next line where it shows all of the possible initializers of NSTimeInterval. What am I missing here?

    Aside: I would love it if there were a well written tutorial on Swift’s type system and doing these kinds of things. The intermediate/advanced stuff is just not well covered in Apple’s sparse Swift documentation

    Update/Clarification:

    What I want is to be able to evaluate any of the above expressions:

    42.seconds   --> 42
    3.14.minutes --> 188.4
    0.5.hours    --> 1800
    13.days      --> 1123200
    

    Furthermore, I want the return type of these to be NSTimeInterval (type alias for Double), such that:

    42.seconds is NSTimeInterval   --> true
    3.14.minutes is NSTimeInterval --> true
    0.5.hours is NSTimeInterval    --> true
    13.days is NSTimeInterval      --> true
    

    I know that I can achieve this by simply extending Double and Int as such:

    extension Int {
        var seconds:NSTimeInterval {
            return NSTimeInterval(self)
        }
    
        var minutes:NSTimeInterval {
            return NSTimeInterval(self * 60)
        }
    
        var hours:NSTimeInterval {
            return NSTimeInterval(self * 3600)
        }
    
        var days:NSTimeInterval {
            return NSTimeInterval(self * 3600 * 24)
        }
    }
    
    extension Double {
        var seconds:NSTimeInterval {
            return NSTimeInterval(self)
        }
    
        var minutes:NSTimeInterval {
            return NSTimeInterval(self * 60)
        }
    
        var hours:NSTimeInterval {
            return NSTimeInterval(self * 3600)
        }
    
        var days:NSTimeInterval {
            return NSTimeInterval(self * 3600 * 24)
        }
    }
    

    But I would also like the following expression to work:

    let foo:Uint = 4242
    foo.minutes                   --> 254520
    foo.minutes is NSTimeInterval --> true
    

    This won’t work though because I have only extended Int, not UInt. I could redundantly extend Uint, and then UInt16, and then Int16, etc….

    I wanted to generalize the extension of Int to IntegerType as shown in the original listing, so that I could just gain the conversions generally for all integer types. And then do the same for FloatingPointType rather than specifically Double. However, that produces the original error. I want to know why I can’t extend IntegerType as generally shown. Are there other IntegerType adopters other than the ones shown in the list, that make it so the NSTimeInterval() initializer does not resolve?

    3 Solutions Collect From Internet About “Trying to extend IntegerType (and FloatingPointType); Why can't all Int types be converted to NSTimeInterval”

    I could redundantly extend Uint, and then UInt16, and then Int16, etc….

    Correct. That is how it’s done today in Swift. You can’t declare that a protocol conforms to another protocol in an extension. (“Why?” “Because the compiler doesn’t allow it.”)

    But that doesn’t mean you have to rewrite all the implementation. You currently have to implement it three times (four times if you want Float80, but that doesn’t seem useful here). First, you declare your protocol.

    import Foundation
    
    // Declare the protocol
    protocol TimeIntervalConvertible {
        func toTimeInterval() -> NSTimeInterval
    }
    
    // Add all the helpers you wanted
    extension TimeIntervalConvertible {
        var seconds:NSTimeInterval {
            return NSTimeInterval(self.toTimeInterval())
        }
    
        var minutes:NSTimeInterval {
            return NSTimeInterval(self.toTimeInterval() * 60)
        }
    
        var hours:NSTimeInterval {
            return NSTimeInterval(self.toTimeInterval() * 3600)
        }
    
        var days:NSTimeInterval {
            return NSTimeInterval(self.toTimeInterval() * 3600 * 24)
        }
    }
    
    // Provide the implementations. FloatingPointType doesn't have an equivalent to
    // toIntMax(). There's no toDouble() or toFloatMax(). Converting a Float to
    // a Double injects data noise in a way that converting Int8 to IntMax does not.
    extension Double {
        func toTimeInterval() -> NSTimeInterval { return NSTimeInterval(self) }
    }
    
    extension Float {
        func toTimeInterval() -> NSTimeInterval { return NSTimeInterval(self) }
    }
    
    extension IntegerType {
        func toTimeInterval() -> NSTimeInterval { return NSTimeInterval(self.toIntMax()) }
    }
    
    // And then we tell it that all the int types can get his implementation
    extension Int: TimeIntervalConvertible {}
    extension Int8: TimeIntervalConvertible {}
    extension Int16: TimeIntervalConvertible {}
    extension Int32: TimeIntervalConvertible {}
    extension Int64: TimeIntervalConvertible {}
    extension UInt: TimeIntervalConvertible {}
    extension UInt8: TimeIntervalConvertible {}
    extension UInt16: TimeIntervalConvertible {}
    extension UInt32: TimeIntervalConvertible {}
    extension UInt64: TimeIntervalConvertible {}
    

    This is how number types are currently done in Swift. Look through stdlib. You’ll see lots of stuff like:

    extension Double {
        public init(_ v: UInt8)
        public init(_ v: Int8)
        public init(_ v: UInt16)
        public init(_ v: Int16)
        public init(_ v: UInt32)
        public init(_ v: Int32)
        public init(_ v: UInt64)
        public init(_ v: Int64)
        public init(_ v: UInt)
        public init(_ v: Int)
    }
    

    Would it be nice in some cases to talk about “number-like things?” Sure. You can’t in Swift today.

    “Why?”

    Because the compiler doesn’t implement it. Some day it may. Until then, create the extension for every type you want it on. A year ago this would have taken even more code.

    Note that while some of this is “Swift doesn’t have that feature yet,” some is also on purpose. Swift intentionally requires explicit conversion between number types. Converting between number types can often lead to losing information or injecting noise, and has historically been a source of tricky bugs. You should be thinking about that every time you convert a number. For example, there’s the obvious case that going from Int64 to Int8 or from UInt8 to Int8 can lose information. But going from Int64 to Double can also lose information. Not all 64-bit integers can be expressed as a Double. This is a subtle fact that burns people quite often when dealing with very large numbers, and Swift encourages you to deal with it. Even converting a Float to a Double injects noise into your data. 1/10 expressed as a Float is a different value than 1/10 expressed as a Double. When you convert the Float to a Double, did you mean to extend the repeating digits or not? You’ll introduce different kinds of error depending on which you pick, so you need to pick.

    Note also that your .days can introduce subtle bugs depending on the exact problem domain. A day is not always 24 hours. It can be 23 hours or 25 hours depending on DST changes. Sometimes that matters. Sometimes it doesn’t. But it’s a reason to be very careful about treating “days” as though it were a specific number of seconds. Usually if you want to work in days, you should be using NSDate, not NSTimeInterval. I’d be very suspicious of that particular one.

    BTW, you may be interested in my old implementation of this idea. Rather than use the syntax:

    1.seconds
    

    I used the syntax:

    1 * Second
    

    And then overloaded multiplication to return a struct rather than a Double. Returning a struct this way gives much better type safety. For example, I could type-check “time * frequency == cycles” and “cycles / time == frequency”, which is something you can’t do with Double. Unfortunately NSTimeInterval isn’t a separate type; it’s just another name for Double. So any method you put on NSTimeInterval is applied to every Double (which is sometimes weird).

    Personally I’d probably solve this whole problem this way:

    let Second: NSTimeInterval = 1
    let Seconds = Second
    let Minute = 60 * Seconds
    let Minutes = Minute
    let Hour = 60 * Minutes
    let Hours = Hour
    
    let x = 100*Seconds
    

    You don’t even need to overload the operators. It’s already done for you.

    NSTimeInterval is just an alias for Double. You can easily achieve what you want by defining your own struct, like this:

    struct MyTimeInterval {
        var totalSecs: NSTimeInterval
    
        var totalHours: NSTimeInterval {
            get {
                return self.totalSecs / 3600.0
            }
            set {
                self.totalSecs = newValue * 3600.0
            }
        }
    
        init(_ secs: NSTimeInterval) {
            self.totalSecs = secs
        }
    
        func totalHourString() -> String {
            return String(format: "%.2f hours", arguments: [self.totalHours])
        }
    }
    
    var t = MyTimeInterval(5400)
    print(t.totalHourString())
    
    extension Int {
        var seconds: Int {
                return self
            }
        var minutes: Int {
            get {
                return self * 60
            }
        }
        var hours: Int {
            get {
                return minutes * 60
            }
        }
        var timeString: String {
            get {
                let seconds = abs(self) % 60
                let minutes = ((abs(self) - seconds) / 60) % 60
                let hours = ((abs(self) - seconds) / 3660)
                let sign = self < 0 ? "-" :" "
                let str = String(format: "\(sign)%2d:%02d:%02d", arguments: [Int(hours),Int(minutes),Int(seconds)])
                return str
            }
        }
    }
    
    6.minutes == 360.seconds                    // true
    
    let t1 = 1.hours + 22.minutes + 12.seconds
    let t2 = 12.hours - 15.minutes
    let time = t1 - t2
    
    print("t1    =",t1.timeString,t1)             // t1    =   1:22:12 4932
    print("t2    =",t2.timeString,t2)             // t2    =  11:45:00 42300
    print("t1-t2 =",time.timeString,time)         // t1-t2 = -10:22:48 -37368
    
    (12.hours / 30.minutes) == 24                 // true