├── .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..