├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Package.swift ├── README.md ├── Sources └── TimeIntervalFormatStyle │ ├── TimeInterval+Constants.swift │ ├── TimeInterval+FormatStyle.swift │ ├── TimeIntervalFormatStyle.swift │ └── TimeIntervalParseStrategy.swift └── Tests └── TimeIntervalFormatStyleTests └── TimeIntervalFormatStyleTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "TimeIntervalFormatStyle", 8 | platforms: [ 9 | .macOS(.v12), 10 | .iOS(.v15), 11 | ], 12 | products: [ 13 | // Products define the executables and libraries a package produces, and make them visible to other packages. 14 | .library( 15 | name: "TimeIntervalFormatStyle", 16 | targets: ["TimeIntervalFormatStyle"]), 17 | ], 18 | dependencies: [ 19 | // Dependencies declare other packages that this package depends on. 20 | // .package(url: /* package url */, from: "1.0.0"), 21 | ], 22 | targets: [ 23 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 24 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 25 | .target( 26 | name: "TimeIntervalFormatStyle", 27 | dependencies: []), 28 | .testTarget( 29 | name: "TimeIntervalFormatStyleTests", 30 | dependencies: ["TimeIntervalFormatStyle"]), 31 | ] 32 | ) 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TimeIntervalFormatStyle 2 | 3 | Simple ParseableFormatStyle conformace for TimeInterval, with and without milliseconds. 4 | 5 | ## Input 6 | 7 | Can accept string timestamps formatted with hours, minutes, seconds, and, optionally, milliseconds. The parser will handle missing 0-padding 8 | 9 | ### Example inputs 10 | - 0:10:45.333 -> Valid 11 | - 0:2:45.333 -> Valid (0-padding missing OK) 12 | - 20:10:45.2 -> Valid (0 padding on milliseconds missing) 13 | - 20:10:45.200 -> Valid (more than 1 hour digit) 14 | - 0:10:45 -> Valid (no milliseconds) 15 | - 0:10:45.22323 -> Valid (milliseconds go beyond thousandths place) 16 | - 0:10:45:220 -> Valid (millisecond delimiter swapped) 17 | -------------------------- 18 | - 10:45.222 -> Invalid (must have hours, even if 0) 19 | - 0:10:45.aaa -> Invalid (non-digit characters) 20 | - 0-10:45.000 -> Invalid (wrong delimiter) 21 | - 1:10:62.222 -> Invalid (too many seconds) 22 | 23 | ## Output 24 | 25 | Using `showsMilliseconds` the client can control if milliseconds are shown or not. All output will be 0-padded and contains exactly 3 places of milliseconds. 26 | 27 | - With milliseconds: 2:03:44.232 28 | - Without milliseconds: 2:03:44 29 | 30 | ## Usage in SwiftUI 31 | 32 | I built this to be able to easily move to/from TimeIntervals in SwiftUI TextFields. A simple example below! 33 | 34 | ```Swift 35 | 36 | struct TimeIntervalTextField: View { 37 | @Binding var duration: TimeInterval 38 | 39 | var body: some View { 40 | TextField("Duration:", value: $duration, format: .timeInterval(showMilliseconds: true), prompt: nil) 41 | } 42 | } 43 | 44 | ``` 45 | -------------------------------------------------------------------------------- /Sources/TimeIntervalFormatStyle/TimeInterval+Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimeInterval+Constants.swift 3 | // TimeIntervalFormatStyle 4 | // 5 | // Created by Sommer Panage on 3/6/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension TimeInterval { 11 | static let secondsPerMinute = 60.0 12 | static let minutesPerHour = 60.0 13 | static let secondsPerHour = secondsPerMinute * minutesPerHour 14 | static let millisecondsPerSecond = 1000.0 15 | } 16 | -------------------------------------------------------------------------------- /Sources/TimeIntervalFormatStyle/TimeInterval+FormatStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimeInterval+ParseableFormatStyle.swift 3 | // TimeIntervalFormatStyle 4 | // 5 | // Created by Sommer Panage on 3/4/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension TimeInterval { 11 | 12 | /// Get a formatted string from a format style 13 | /// - Parameter formatStyle: Time Interval Format Style 14 | /// - Returns: Formatted string 15 | func formatted(_ formatStyle: TimeIntervalFormatStyle) -> String { 16 | formatStyle.format(self) 17 | } 18 | } 19 | 20 | public extension FormatStyle where Self == TimeInterval.TimeIntervalFormatStyle { 21 | 22 | /// Format the given string as a time interval in the format 7:54:33.632 or similar 23 | /// - Parameter showMilliseconds: Shows millieconds. Ex: 1:03:44:789 . Default == `false` 24 | static func timeInterval(showMilliseconds: Bool = false ) -> TimeInterval.TimeIntervalFormatStyle { 25 | TimeInterval.TimeIntervalFormatStyle(showMilliseconds) 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /Sources/TimeIntervalFormatStyle/TimeIntervalFormatStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimeIntervalFormatStyle.swift 3 | // TimeIntervalFormatStyle 4 | // 5 | // Created by Sommer Panage on 3/6/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension TimeInterval { 11 | struct TimeIntervalFormatStyle { 12 | 13 | private var showMilliseconds: Bool = false 14 | 15 | /// Constructer to allow extensions to set formatting 16 | /// - Parameter showMilliseconds: Shows millieconds. Ex: 1:03:44:789 . Default == `false` 17 | init(_ showMilliseconds: Bool) { 18 | self.showMilliseconds = showMilliseconds 19 | } 20 | } 21 | } 22 | 23 | extension TimeInterval.TimeIntervalFormatStyle: ParseableFormatStyle { 24 | 25 | /// A `ParseStrategy` that can be used to parse this `FormatStyle`'s output 26 | public var parseStrategy: TimeIntervalParseStrategy { 27 | return TimeIntervalParseStrategy() 28 | } 29 | 30 | /// Returns a string based on an input time interval. String format may include milliseconds or not 31 | /// Example: "2:33:29.632" aka 2 hours, 33 minutes, 29.632 seconds 32 | public func format(_ value: TimeInterval) -> String { 33 | let hour = Int((value / TimeInterval.secondsPerHour).rounded(.towardZero)) 34 | let minute = Int((value / TimeInterval.secondsPerMinute).truncatingRemainder(dividingBy: TimeInterval.minutesPerHour)) 35 | let second = Int(value.truncatingRemainder(dividingBy: TimeInterval.secondsPerMinute)) 36 | if showMilliseconds { 37 | let millisecond = Int((value * TimeInterval.millisecondsPerSecond).truncatingRemainder(dividingBy: TimeInterval.millisecondsPerSecond)) 38 | return String(format:"%d:%02d:%02d.%03d", hour, minute, second, millisecond) // ex: 10:04:09.689 39 | } else { 40 | return String(format:"%d:%02d:%02d", hour, minute, second) // ex: 10:04:09 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/TimeIntervalFormatStyle/TimeIntervalParseStrategy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimeIntervalParseStrategy.swift 3 | // TimeIntervalFormatStyle 4 | // 5 | // Created by Sommer Panage on 3/6/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct TimeIntervalParseStrategy: ParseStrategy { 11 | /// Multipliers for each unit in order as they'd be read L to R: hours, minutes, seconds, milliseconds. 12 | private static let multipliers = [TimeInterval.secondsPerHour, TimeInterval.secondsPerMinute, 1.0, 1.0 / TimeInterval.millisecondsPerSecond] 13 | 14 | /// Max values for each unit in order as they'd be read L to R: hours, minutes, seconds, milliseconds. 15 | private static let maxValues = [Double.greatestFiniteMagnitude, TimeInterval.minutesPerHour - 1, TimeInterval.secondsPerMinute - 1, TimeInterval.millisecondsPerSecond - 1] 16 | 17 | /// Creates an instance of the `ParseOutput` type from `value`. 18 | /// - Parameter value: Value to convert to `TimeInterval` 19 | /// - Returns: `TimeInterval` 20 | public func parse(_ value: String) throws -> TimeInterval { 21 | var timeComponents = value.components(separatedBy: CharacterSet(charactersIn: ":.")) 22 | guard timeComponents.count <= TimeIntervalParseStrategy.multipliers.count else { return 0 } 23 | 24 | // If we have milliseconds (i.e. all 4 time component), we need to 25 | // coerce it to exactly 3 digits, since we only manage down to milliseconds 26 | if timeComponents.count == TimeIntervalParseStrategy.multipliers.count { 27 | let requiredDigits = 3 28 | var millisecondsString = timeComponents[timeComponents.count - 1] 29 | let digitCount = millisecondsString.count 30 | if digitCount < requiredDigits { 31 | millisecondsString.append(String(repeating: "0", count: requiredDigits - digitCount)) 32 | } else if digitCount > requiredDigits { 33 | millisecondsString.removeLast(digitCount - requiredDigits) 34 | } 35 | timeComponents[timeComponents.count - 1] = millisecondsString 36 | } 37 | 38 | // Now go through our component strings and calculate our TimeInterval. 39 | var time = 0.0 40 | for i in 0..