TimeSpan.swift

//
//  TimeSpan.swift
//  SeatNinjaBase
//
//  Created by odyth on 12/15/16.
//  Copyright © 2016 SeatNinja Inc. All rights reserved.
//

import Foundation

public struct TimeSpan: Comparable, Equatable, CustomStringConvertible, Hashable, Codable
{
    public static let maxValue = TimeSpan(ticks: Int64.max)
    public static let minValue = TimeSpan(ticks: Int64.min)
    public static let zero = TimeSpan()
    
    public private(set) var ticks: Int64 = 0
    
    public init()
    {
        self.ticks = 0
    }
    
    public init(ticks: Int64)
    {
        self.ticks = ticks
    }
    
    public init(hours: Int, minutes: Int, seconds: Int)
    {
        // totalSeconds is bounded by 2^31 * 2^12 + 2^31 * 2^8 + 2^31,
        // which is less than 2^44, meaning we won't overflow totalSeconds.
        let totalSeconds = Int64(hours) * 3600 + Int64(minutes) * 60 + Int64(seconds)
        assert(totalSeconds <= Time.maxSeconds && totalSeconds >= Time.minSeconds)
        self.ticks = totalSeconds * Time.ticksPerSecond
    }
    
    public init(days: Int, hours: Int, minutes: Int, seconds: Int)
    {
        self.init(days:days, hours:hours, minutes:minutes, seconds:seconds, milliseconds:0)
    }
    
    public init(days: Int, hours: Int, minutes: Int, seconds: Int, milliseconds: Int)
    {
        let totalMilliSeconds:Int64 = (Int64(days) * Int64(Time.oneDay) +
                                      Int64(hours) * Int64(Time.oneHour) +
                                      Int64(minutes) * Int64(Time.oneMinute) + Int64(seconds)) * 1000 +
                                      Int64(milliseconds)
        
        assert(totalMilliSeconds <= Time.maxMilliSeconds && totalMilliSeconds >= Time.minMilliSeconds)
        self.init(ticks:totalMilliSeconds * Time.ticksPerMillisecond)
    }
    
    public var days: Int
    {
        return Int(ticks / Time.ticksPerDay )
    }
    
    public var hours: Int
    {
        return Int((ticks / Time.ticksPerHour) % 24)
    }
    
    public var minutes: Int
    {
        return Int((ticks / Time.ticksPerMinute) % 60)
    }
    
    public var seconds: Int
    {
        return Int((ticks / Time.ticksPerSecond) % 60)
    }
    
    public var milliseconds: Int
    {
        return Int((ticks / Time.ticksPerMillisecond) % 1000)
    }
    
    public var totalDays: Double
    {
        return Double(ticks) * Time.daysPerTick
    }
    
    public var totalHours: Double
    {
        return Double(ticks) * Time.hoursPerTick
    }
    
    public var totalMinutes: Double
    {
        return Double(ticks) * Time.minutesPerTick
    }
    
    public var totalSeconds: Double
    {
        return Double(ticks) * Time.secondsPerTick
    }
    
    public var totalMilliseconds: Double
    {
        let temp = Double(ticks) * Time.millisecondsPerTick
        if temp > Double(Time.maxMilliSeconds)
        {
            return Double(Time.maxMilliSeconds)
        }
        
        if temp < Double(Time.minMilliSeconds)
        {
            return Double(Time.minMilliSeconds)
        }
        return temp
    }
    
    public var timeInterval: TimeInterval
    {
        return totalSeconds
    }
    
    public var isMaxValue: Bool
    {
        return ticks == TimeSpan.maxValue.ticks
    }
    
    public var isMinValue: Bool
    {
        return ticks == TimeSpan.minValue.ticks
    }
    
    public func add(timeSpan: TimeSpan) -> TimeSpan
    {
        let result = ticks.addingReportingOverflow(timeSpan.ticks)
        assert(result.overflow == false)
        return TimeSpan(ticks:result.partialValue)
    }
    
    public func subtract(timeSpan: TimeSpan) -> TimeSpan
    {
        let result = ticks.subtractingReportingOverflow(timeSpan.ticks)
        assert(result.overflow == false)
        return TimeSpan(ticks:result.partialValue)
    }
    
    public func duration() -> TimeSpan
    {
        assert(ticks != TimeSpan.minValue.ticks, "overflow duration")
        return TimeSpan(ticks: ticks >= 0 ? ticks : -ticks)
    }
    
    public func negate() -> TimeSpan
    {
        assert(ticks != TimeSpan.minValue.ticks, "overflow negate")
        return TimeSpan(ticks:-ticks)
    }
    
    public static func from(days: Double) -> TimeSpan
    {
        return interval(days, scale:Time.millisPerDay)
    }
    
    public static func from(hours: Double) -> TimeSpan
    {
        return interval(hours, scale:Time.millisPerHour)
    }
    
    public static func from(minutes: Double) -> TimeSpan
    {
        return interval(minutes, scale:Time.millisPerMinute)
    }
    
    public static func from(seconds: Double) -> TimeSpan
    {
        return interval(seconds, scale:Time.millisPerSecond)
    }
    
    public static func from(milliseconds: Double) -> TimeSpan
    {
        return interval(milliseconds, scale:1)
    }
    
    public static func from(ticks: Int64) -> TimeSpan
    {
        return TimeSpan(ticks:ticks)
    }
    
    public static func from(string: String) -> TimeSpan
    {
        return TimeSpan(ticks: parse(string: string))
    }
    
    private static func interval(_ value: Double, scale: Int) -> TimeSpan
    {
        assert(Double.nan != value, "value cannot be nan")
        
        let tmp = value * Double(scale)
        let millis = tmp + (value >= 0 ? 0.5 : -0.5)
        assert(!(Int64(millis) > Int64.max / Time.ticksPerMillisecond) || (Int64(millis) < Int64.min / Time.ticksPerMillisecond), "timespan too long")
        
        let result = Int64(millis) * Time.ticksPerMillisecond
        return TimeSpan(ticks:result)
    }
    
    private static func parse(string: String) -> Int64
    {
        var days: Int64 = 0
        var hours: Int64 = 0
        var minutes: Int64 = 0
        var seconds: Int64 = 0
        var milliseconds: Int64 = 0
        
        let colonParts = string.components(separatedBy: ":")
        let hoursString = colonParts.first!
        if hoursString.range(of: ".") != nil
        {
            let hourParts = hoursString.components(separatedBy: ".")
            days = Int64(hourParts[0])!
            hours = Int64(hourParts[1])!
        }
        else
        {
            hours = Int64(hoursString)!
        }
        minutes = Int64(colonParts[1])!
        
        var millisecondsString: String? = nil
        if colonParts.count == 3
        {
            let secondsString = colonParts.last!
            if secondsString .range(of: ".") != nil
            {
                let secondParts = secondsString.components(separatedBy: ".")
                seconds = Int64(secondParts[0])!
                millisecondsString = secondParts[1]
                milliseconds = Int64(millisecondsString!)!
            }
            else
            {
                seconds = Int64(secondsString)!
            }
        }
        
        var time = abs(days) * 3600 * 24
        time += abs(hours) * 3600
        time += abs(minutes) * 60
        time += abs(seconds)
        time *= 1000

        if milliseconds != 0
        {
            var lowerLimit = Time.ticksPerTenthSecond
            if millisecondsString![0] == "0"
            {
                var i: Int = 0
                var divisor: Int64 = 10
                while millisecondsString![i] == "0"
                {
                    divisor *= 10
                    i += 1
                }
                lowerLimit /= divisor
            }
            while milliseconds < lowerLimit
            {
                milliseconds *= 10
            }
        }

        let result = milliseconds.addingReportingOverflow(time * Time.ticksPerMillisecond)
        var ticks = result.partialValue
        if string[0] == "-"
        {
            if result.overflow == false
            {
                ticks = -ticks
            }
        }
        else
        {
            assert(result.overflow == false)
        }
        
        return ticks
    }
    
    //MARK: - CustomStringConvertible
    
    public var description: String
    {
        let days = abs(self.days)
        let hours = abs(self.hours)
        let minutes = abs(self.minutes)
        let seconds = abs(self.seconds)
        let fraction = abs(ticks % Time.ticksPerSecond)
        
        var result = "\(String(format: "%02d", hours)):\(String(format: "%02d", minutes)):\(String(format: "%02d", seconds))"
        
        if days != 0
        {
            result = "\(days).\(result)"
        }
        if fraction != 0
        {
            result = "\(result).\(String(format: "%07d", fraction))"
        }
        return ticks < 0 ? "-\(result)" : result
    }
    
    //MARK: - Operators
    
    public static prefix func +(_ t: TimeSpan) -> TimeSpan
    {
        return t
    }
    
    public static func +=(left: inout TimeSpan, right: TimeSpan)
    {
        left = left + right
    }
    
    public static func +(left: TimeSpan, right: TimeSpan) -> TimeSpan
    {
        return left.add(timeSpan:right)
    }
    
    public static prefix func -(_ t: TimeSpan) -> TimeSpan
    {
        assert(t != TimeSpan.minValue)
        return TimeSpan(ticks: -t.ticks)
    }
    
    public static func -(left: TimeSpan, right: TimeSpan) -> TimeSpan
    {
        return left.subtract(timeSpan:right)
    }
    
    public static func -=(left: inout TimeSpan, right: TimeSpan)
    {
        left = left - right
    }
    
    //Mark: - Hashable
    
    public var hashValue: Int
    {
        return Int(ticks) ^ Int(ticks >> 32)
    }
    
    //MARK: - Equatable
    
    public static func == (left: TimeSpan, right: TimeSpan) -> Bool
    {
        return left.ticks == right.ticks
    }
    
    //MARK: - Comparable
    
    public static func <(left: TimeSpan, right: TimeSpan) -> Bool
    {
        return left.ticks < right.ticks
    }
    
    //MARK: - Codable
    
    public init(from decoder: Decoder) throws
    {
        let container = try! decoder.singleValueContainer()
        if let string = try? container.decode(String.self)
        {
            ticks = TimeSpan.parse(string: string)
        }
        else
        {
            ticks = 0
        }
    }
    
    public func encode(to encoder: Encoder) throws
    {
        var container = encoder.singleValueContainer()
        try! container.encode(description)
    }
}

See Time.swift