diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..a3ef46eb
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,72 @@
+# Xcode
+
+# macOS
+.DS_Store
+
+## Build generated
+build/
+DerivedData/
+
+## Various settings
+*.pbxuser
+!default.pbxuser
+*.mode1v3
+!default.mode1v3
+*.mode2v3
+!default.mode2v3
+*.perspectivev3
+!default.perspectivev3
+xcuserdata/
+
+## Other
+*.moved-aside
+*.xccheckout
+*.xcscmblueprint
+
+## Obj-C/Swift specific
+*.hmap
+*.ipa
+*.dSYM.zip
+*.dSYM
+
+## Playgrounds
+timeline.xctimeline
+playground.xcworkspace
+
+## SPM support in Xcode
+.swiftpm
+
+# Swift Package Manager
+#
+# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
+Packages/
+Package.pins
+Package.resolved
+.build/
+
+# CocoaPods
+#
+# We recommend against adding the Pods directory to your .gitignore. However
+# you should judge for yourself, the pros and cons are mentioned at:
+# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
+
+Pods/
+
+# Carthage
+#
+# Add this line if you want to avoid checking in source code from Carthage dependencies.
+# Carthage/Checkouts
+
+Carthage/Build
+
+# fastlane
+#
+# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
+# screenshots whenever they are needed.
+# For more information about the recommended setup visit:
+# https://docs.fastlane.tools/best-practices/source-control/#source-control
+
+fastlane/report.xml
+fastlane/Preview.html
+fastlane/screenshots/**/*.png
+fastlane/test_output
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..42ebacb4
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018 Steffan Andrews - https://github.com/orchetect
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/Package.swift b/Package.swift
new file mode 100644
index 00000000..f6929ce8
--- /dev/null
+++ b/Package.swift
@@ -0,0 +1,38 @@
+// swift-tools-version:5.3
+// The swift-tools-version declares the minimum version of Swift required to build this package.
+
+import PackageDescription
+
+let package = Package(
+
+ name: "SwiftTimecode",
+
+ products: [
+ // Products define the executables and libraries a package produces, and make them visible to other packages.
+ .library(
+ name: "SwiftTimecode",
+ targets: ["SwiftTimecode"])
+ ],
+
+ dependencies: [
+ // Dependencies declare other packages that this package depends on.
+ // .package(url: /* package url */, from: "1.0.0"),
+ ],
+
+ targets: [
+ // Targets are the basic building blocks of a package. A target can define a module or a test suite.
+ // Targets can depend on other targets in this package, and on products in packages this package depends on.
+ .target(
+ name: "SwiftTimecode",
+ dependencies: []),
+ .testTarget(
+ name: "SwiftTimecode-Unit-Tests",
+ dependencies: ["SwiftTimecode"]),
+
+ .testTarget(
+ name: "SwiftTimecode-Dev-Tests",
+ dependencies: ["SwiftTimecode"]
+ )
+ ]
+
+)
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..36e46a27
--- /dev/null
+++ b/README.md
@@ -0,0 +1,64 @@
+# SwiftTimecode
+
+
+
+
+
+
+
+
+
+A robust library for working with SMPTE timecode supporting 20 industry frame rates, and including methods to convert to/from timecode strings and perform calculations.
+
+## Supported Frame Rates
+
+| NTSC | PAL | HD / Film | Other |
+| --------- | ---- | --------- | ------ |
+| 29.97 | 25 | 23.976 | 30 |
+| 29.97 DF | 50 | 24 | 30 DF |
+| 59.94 | 100 | 24.98 | 60 |
+| 59.94 DF | | 47.952 | 60 DF |
+| 119.88 | | 48 | 120 |
+| 119.88 DF | | | 120 DF |
+
+## Core Features
+
+- Convert timecode to string, or a timecode string to values
+- Convert timecode to real wall-clock time, and vice-versa
+- Convert timecode to # of samples at any audio sample-rate, and vice-versa
+- Granular timecode validation
+- A Formatter object that can format timecode and also provide an NSAttributedString showing invalid timecode components in an alternate color (such as red)
+- Support for Days as a timecode component (which Cubase supports as part of its timecode format)
+- Support for sub-frames
+- Common math operations between timecodes: add, subtract, multiply, divide
+- Exhaustive unit tests ensuring accuracy
+
+## Development Status
+
+### Incomplete Features (Still in Development)
+
+- [ ] Complete sub-frame support
+
+ - [ ] Test subFrameDivisor effect when set to -1, 0, 1, or 1000000
+ - [ ] Needs to be added to String getters/setters
+ - Add 100-120 fps 3-digit frames display support
+
+- [ ] Add method to convert to another framerate by producing a new `OTTimecode` object. But make it clear that it's a LOSSY process. Suggested API:
+
+ ```swift
+ func convert(to: FrameRate, limit: UpperLimit) -> OTTimecode
+ ```
+
+### Maintenance
+
+- [ ] Add code examples to README.md or wiki.
+
+### Future Features Planned
+
+- None at this time.
+
+## Known Issues
+
+- The Dev Tests are not meant to be run as routine unit tests, but are designed as a test harness to be used only when altering critical parts of the library to ensure stability of internal calculations.
+- Unit Tests won't build/run for watchOS Simulator because XCTest does not work on watchOS
+ - Workaround: Don't run unit tests for a watchOS target. Using macOS or iOS as a unit test target should be sufficient enough. If anyone runs into issues with the library on watchOS, feel free to contribute a solution or fix anything that requires fixing.
diff --git a/Sources/SwiftTimecode/Components/Component.swift b/Sources/SwiftTimecode/Components/Component.swift
new file mode 100644
index 00000000..0b531e16
--- /dev/null
+++ b/Sources/SwiftTimecode/Components/Component.swift
@@ -0,0 +1,25 @@
+//
+// Component.swift
+// SwiftTimecode
+//
+// Created by Steffan Andrews on 2020-06-15.
+// Copyright © 2020 Steffan Andrews. All rights reserved.
+//
+
+import Foundation
+
+extension OTTimecode {
+
+ /// Enum naming an individual timecode component
+ public enum Component {
+
+ case days
+ case hours
+ case minutes
+ case seconds
+ case frames
+ case subFrames
+
+ }
+
+}
diff --git a/Sources/SwiftTimecode/Components/Components.swift b/Sources/SwiftTimecode/Components/Components.swift
new file mode 100644
index 00000000..2200d593
--- /dev/null
+++ b/Sources/SwiftTimecode/Components/Components.swift
@@ -0,0 +1,121 @@
+//
+// Components.swift
+// SwiftTimecode
+//
+// Created by Steffan Andrews on 2018-07-20.
+// Copyright © 2018 Steffan Andrews. All rights reserved.
+//
+
+import Foundation
+
+/// Convenience typealias for cleaner code.
+public typealias TCC = OTTimecode.Components
+
+extension OTTimecode {
+
+ /// Primitive struct that can contain timecode values, agnostic of frame rate.
+ /// Raw values are stored and are not internally limited or clamped.
+ /// The global typealias TCC() is also available for convenience.
+ public struct Components {
+
+ // MARK: Contents
+
+ /// Days
+ public var d: Int
+
+ /// Hours
+ public var h: Int
+
+ /// Minutes
+ public var m: Int
+
+ /// Seconds
+ public var s: Int
+
+ /// Frames
+ public var f: Int
+
+ /// Subframe component (expressed as unit interval 0.0...1.0)
+ public var sf: Int
+
+ // MARK: init
+
+ public init(d: Int = 0,
+ h: Int = 0,
+ m: Int = 0,
+ s: Int = 0,
+ f: Int = 0,
+ sf: Int = 0)
+ {
+ self.d = d
+ self.h = h
+ self.m = m
+ self.s = s
+ self.f = f
+ self.sf = sf
+ }
+
+ }
+
+}
+
+extension OTTimecode.Components: Equatable {
+
+ public static func ==(lhs: Self, rhs: Self) -> Bool {
+ return lhs.d == rhs.d &&
+ lhs.h == rhs.h &&
+ lhs.m == rhs.m &&
+ lhs.s == rhs.s &&
+ lhs.f == rhs.f &&
+ lhs.sf == rhs.sf
+ }
+
+}
+
+extension OTTimecode.Components {
+
+ /// Returns an instance of `OTTimecode(exactly:)`.
+ public func toTimecode(at frameRate: OTTimecode.FrameRate,
+ limit: OTTimecode.UpperLimit = ._24hours,
+ subFramesDivisor: Int? = nil) -> OTTimecode?
+ {
+
+ if let sfd = subFramesDivisor {
+
+ return OTTimecode(self,
+ at: frameRate,
+ limit: limit,
+ subFramesDivisor: sfd)
+
+ } else {
+
+ return OTTimecode(self,
+ at: frameRate,
+ limit: limit)
+
+ }
+ }
+
+ /// Returns an instance of `OTTimecode(rawValues:)`.
+ public func toTimecode(rawValuesAt frameRate: OTTimecode.FrameRate,
+ limit: OTTimecode.UpperLimit = ._24hours,
+ subFramesDivisor: Int? = nil) -> OTTimecode?
+ {
+
+ if let sfd = subFramesDivisor {
+
+ return OTTimecode(rawValues: self,
+ at: frameRate,
+ limit: limit,
+ subFramesDivisor: sfd)
+
+ } else {
+
+ return OTTimecode(rawValues: self,
+ at: frameRate,
+ limit: limit)
+
+ }
+ }
+
+}
diff --git a/Sources/SwiftTimecode/Data Interchange/OTTimecode Components.swift b/Sources/SwiftTimecode/Data Interchange/OTTimecode Components.swift
new file mode 100644
index 00000000..b0346a98
--- /dev/null
+++ b/Sources/SwiftTimecode/Data Interchange/OTTimecode Components.swift
@@ -0,0 +1,86 @@
+//
+// OTTimecode Components.swift
+// SwiftTimecode
+//
+// Created by Steffan Andrews on 2020-06-15.
+// Copyright © 2020 Steffan Andrews. All rights reserved.
+//
+
+import Foundation
+
+extension OTTimecode {
+
+ /// Timecode components.
+ /// When setting, invalid values will cause the setter to fail silently. (Validation is based on the frame rate and `upperLimit` property.)
+ public var components: Components {
+ get {
+ return Components(d: days,
+ h: hours,
+ m: minutes,
+ s: seconds,
+ f: frames,
+ sf: subFrames)
+ }
+ set {
+ _ = setTimecode(exactly: newValue)
+ }
+ }
+
+ /** Set timecode from tuple values.
+ Returns true/false depending on whether the string values are valid or not.
+ Values which are out-of-bounds will return false. (Validation is based on the frame rate and `upperLimit` property.)
+ */
+ @discardableResult
+ public mutating func setTimecode(exactly values: Components) -> Bool {
+
+ guard values.invalidComponents(at: frameRate,
+ limit: upperLimit,
+ subFramesDivisor: subFramesDivisor).count == 0
+ else { return false }
+
+ days = values.d
+ hours = values.h
+ minutes = values.m
+ seconds = values.s
+ frames = values.f
+ subFrames = values.sf
+
+ return true
+ }
+
+ /** Set timecode from tuple values.
+ (Validation is based on the frame rate and `upperLimit` property.)
+ */
+ public mutating func setTimecode(clamping values: Components) {
+ days = values.d
+ hours = values.h
+ minutes = values.m
+ seconds = values.s
+ frames = values.f
+ subFrames = values.sf
+
+ clampComponents()
+ }
+
+ /** Set timecode from tuple values.
+ Timecode will wrap if out-of-bounds. Will handle negative values and wrap accordingly. (Wrapping is based on the frame rate and `upperLimit` property.)
+ */
+ public mutating func setTimecode(wrapping values: Components) {
+ // guaranteed to work so we can ignore the value returned
+
+ _ = setTimecode(exactly: __add(wrapping: values, to: Components(f: 0)))
+ }
+
+ /** Set timecode from tuple values.
+ Timecode values will not be validated or rejected if they overflow.
+ */
+ public mutating func setTimecode(rawValues values: Components) {
+ days = values.d
+ hours = values.h
+ minutes = values.m
+ seconds = values.s
+ frames = values.f
+ subFrames = values.sf
+ }
+
+}
diff --git a/Sources/SwiftTimecode/Data Interchange/OTTimecode Real Time.swift b/Sources/SwiftTimecode/Data Interchange/OTTimecode Real Time.swift
new file mode 100644
index 00000000..e5df0411
--- /dev/null
+++ b/Sources/SwiftTimecode/Data Interchange/OTTimecode Real Time.swift
@@ -0,0 +1,87 @@
+//
+// OTTimecode Real Time.swift
+// SwiftTimecode
+//
+// Created by Steffan Andrews on 2018-07-20.
+// Copyright © 2018 Steffan Andrews. All rights reserved.
+//
+
+import Foundation
+
+extension OTTimecode {
+
+ /// Returns the current timecode converted to a duration in real-time milliseconds (wall-clock time), based on the frame rate. Value is returned as a Double so a high level of precision can be maintained.
+ /// Generally, `.realTime` -> `setTimecode(from: OTTime)` will produce equivalent results where 'from timecode' will == 'out timecode'.
+ /// When setting, invalid values will cause the setter to fail silently. (Validation is based on the frame rate and `upperLimit` property.)
+ public var realTime: OTTime {
+
+ get {
+ var calc = Double(totalElapsedFrames) * (1000.0 / frameRate.frameRateForRealTimeCalculation)
+
+ // over-estimate so real time is just past the equivalent timecode
+ // so calculations of real time back into timecode work reliably
+ // otherwise, this math produces a real time value that can be a hair under the actual elapsed real time that would trigger the equivalent timecode
+
+ calc += 0.00001
+
+ return OTTime(ms: calc)
+ }
+
+ set {
+ _ = setTimecode(from: newValue)
+ }
+
+ }
+
+ /// Sets the timecode to the nearest frame at the current frame rate from real-time milliseconds.
+ /// Returns false if it underflows or overflows valid timecode range.
+ @discardableResult
+ public mutating func setTimecode(from realTimeValue: OTTime) -> Bool {
+
+ // the basic calculation
+ var calc = realTimeValue.ms / (1000.0 / frameRate.frameRateForRealTimeCalculation)
+
+ // over-estimate so real time is just past the equivalent timecode
+ // so calculations of real time back into timecode work reliably
+ // otherwise, this math produces a real time value that can be a hair under the actual elapsed real time that would trigger the equivalent timecode
+
+ calc += 0.0006
+
+ // final calculation
+
+ let elapsedFrames = calc
+ let convertedComponents = Self.components(from: elapsedFrames,
+ at: frameRate,
+ subFramesDivisor: subFramesDivisor)
+
+ return setTimecode(exactly: convertedComponents)
+
+ }
+
+}
+
+extension OTTime {
+
+ /// Convenience method to create an `OTTimecode` struct using the default `(_ exactly:)` initializer.
+ public func toTimecode(at frameRate: OTTimecode.FrameRate,
+ limit: OTTimecode.UpperLimit = ._24hours,
+ subFramesDivisor: Int? = nil) -> OTTimecode?
+ {
+ if let sfd = subFramesDivisor {
+
+ return OTTimecode(self,
+ at: frameRate,
+ limit: limit,
+ subFramesDivisor: sfd)
+
+ } else {
+
+ return OTTimecode(self,
+ at: frameRate,
+ limit: limit)
+
+ }
+
+ }
+
+}
diff --git a/Sources/SwiftTimecode/Data Interchange/OTTimecode Samples.swift b/Sources/SwiftTimecode/Data Interchange/OTTimecode Samples.swift
new file mode 100644
index 00000000..ea101f9c
--- /dev/null
+++ b/Sources/SwiftTimecode/Data Interchange/OTTimecode Samples.swift
@@ -0,0 +1,137 @@
+//
+// OTTimecode Samples.swift
+// SwiftTimecode
+//
+// Created by Steffan Andrews on 2020-06-15.
+// Copyright © 2020 Steffan Andrews. All rights reserved.
+//
+
+import Foundation
+
+extension OTTimecode {
+
+ /// (Lossy)
+ /// Returns the current timecode converted to a duration in real-time audio samples at the given sample rate, rounded to the nearest sample.
+ /// Sample rate must be expressed as an Integer of Hz (ie: 48KHz would be passed as 48000)
+ public func samplesValue(atSampleRate: Int) -> Double {
+
+ // prepare coefficients
+
+ var fRate = frameRate.frameRateForElapsedFramesCalculation
+
+ if frameRate.isDrop
+ && frameRate != ._30_drop
+ && frameRate != ._60_drop
+ && frameRate != ._120_drop {
+ // all dropframe rates require this except 30 DF and its multiples
+ fRate = Double(frameRate.maxFrames) / 1.001
+ }
+
+ var offset = 1.0
+ switch frameRate {
+ case ._23_976,
+ ._24_98,
+ ._29_97,
+ ._47_952,
+ ._59_94,
+ ._119_88:
+ offset = 1.001 // not sure why this works, but it makes for an accurate calculation
+
+ case ._24,
+ ._25,
+ ._29_97_drop,
+ ._30,
+ ._48,
+ ._50,
+ ._59_94_drop,
+ ._60,
+ ._100,
+ ._119_88_drop,
+ ._120:
+ break
+
+ case ._30_drop,
+ ._60_drop,
+ ._120_drop:
+ offset = 0.999
+
+ }
+
+ // perform calculation
+
+ var dbl = totalElapsedFrames * (Double(atSampleRate) / fRate * offset)
+
+ // over-estimate so samples are just past the equivalent timecode
+ // so calculations of samples back into timecode work reliably
+ // otherwise, this math produces a samples value that can be a hair under the actual elapsed samples that would trigger the equivalent timecode
+
+ dbl += 0.0001
+
+ return dbl
+
+ }
+
+ /// (Lossy)
+ /// Sets the timecode to the nearest frame at the current frame rate from elapsed audio samples.
+ /// Returns false if it underflows or overflows valid timecode range.
+ /// Sample rate must be expressed as an Integer of Hz (ie: 48KHz would be passed as 48000)
+ @discardableResult
+ public mutating func setTimecode(fromSamplesValue: Double,
+ atSampleRate: Int) -> Bool {
+
+ // prepare coefficients
+
+ var fRate = frameRate.frameRateForElapsedFramesCalculation
+
+ if frameRate.isDrop
+ && frameRate != ._30_drop
+ && frameRate != ._60_drop
+ && frameRate != ._120_drop {
+ // all dropframe rates require this except 30 DF and its multiples
+ fRate = Double(frameRate.maxFrames) / 1.001
+ }
+
+ var offset = 1.0
+ switch frameRate {
+ case ._23_976,
+ ._24_98,
+ ._29_97,
+ ._47_952,
+ ._59_94,
+ ._119_88:
+ offset = 1.001
+
+ case ._24,
+ ._25,
+ ._29_97_drop,
+ ._30,
+ ._48,
+ ._50,
+ ._59_94_drop,
+ ._60,
+ ._100,
+ ._119_88_drop,
+ ._120:
+ break
+
+ case ._30_drop,
+ ._60_drop,
+ ._120_drop:
+ offset = 0.999
+
+ }
+
+ // perform calculation
+
+ let dbl = fromSamplesValue / (Double(atSampleRate) / fRate * offset)
+
+ // then derive components
+ let convertedComponents = Self.components(from: dbl,
+ at: frameRate,
+ subFramesDivisor: subFramesDivisor)
+
+ return setTimecode(exactly: convertedComponents)
+
+ }
+
+}
diff --git a/Sources/SwiftTimecode/Data Interchange/OTTimecode String.swift b/Sources/SwiftTimecode/Data Interchange/OTTimecode String.swift
new file mode 100644
index 00000000..e54259b1
--- /dev/null
+++ b/Sources/SwiftTimecode/Data Interchange/OTTimecode String.swift
@@ -0,0 +1,337 @@
+//
+// OTTimecode stringValue.swift
+// SwiftTimecode
+//
+// Created by Steffan Andrews on 2020-06-15.
+// Copyright © 2020 Steffan Andrews. All rights reserved.
+//
+
+import Foundation
+
+#if os(macOS)
+import Cocoa
+#elseif os(iOS) || os(tvOS) || os(watchOS)
+import UIKit
+#endif
+
+extension OTTimecode {
+
+
+ // MARK: stringValue
+
+ /** Timecode string representation.
+
+ Valid formats for 24-hour:
+
+ ```
+ "00:00:00:00" "00:00:00;00"
+ "0:00:00:00" "0:00:00;00"
+ "00000000"
+ ```
+
+ Valid formats for 100-day: All of the above, as well as:
+
+ ```
+ "0 00:00:00:00" "0 00:00:00;00"
+ "0:00:00:00:00" "0:00:00:00;00"
+ ```
+
+ When setting, an improperly formatted timecode string or one with invalid values will cause the setter to fail silently. (Validation is based on the frame rate and `upperLimit` property.)
+ */
+ public var stringValue: String {
+ get {
+ let sepDays = " "
+ let sepMain = ":"
+ let sepFrames = frameRate.isDrop ? ";" : ":"
+ let sepSubFrames = "."
+
+ var output = ""
+
+ output += "\(days != 0 ? "\(days)\(sepDays)" : "")"
+ output += "\(String(format: "%02d", hours ))\(sepMain)"
+ output += "\(String(format: "%02d", minutes))\(sepMain)"
+ output += "\(String(format: "%02d", seconds))\(sepFrames)"
+ output += "\(String(format: "%0\(frameRate.numberOfDigits)d", frames))"
+
+ if displaySubFrames {
+ let numberOfSubFramesDigits = validRange(of: .subFrames).upperBound.numberOfDigits
+
+ output += "\(sepSubFrames)\(String(format: "%0\(numberOfSubFramesDigits)d", subFrames))"
+ }
+
+ return output
+ }
+ set {
+ _ = setTimecode(exactly: newValue)
+ }
+ }
+
+ /// Forms `.stringValue` using filename-compatible characters.
+ public var stringValueFileNameCompatible: String {
+ let result = stringValue
+ .replacingOccurrences(of: ":", with: "-")
+ .replacingOccurrences(of: ";", with: "-")
+ .replacingOccurrences(of: " ", with: "-")
+
+ return result
+ }
+
+ // MARK: stringValueValidated
+
+ /// Returns `stringValue` as `NSAttributedString`, highlighting invalid values.
+ ///
+ /// `invalidAttributes` are the `NSAttributedString` attributes applied to invalid values.
+ /// If `invalidAttributes` are not passed, the default of red forground color is used.
+ public func stringValueValidated(invalidAttributes: [NSAttributedString.Key : Any]? = nil,
+ withDefaultAttributes attrs: [NSAttributedString.Key : Any]? = nil) -> NSAttributedString
+ {
+ let sepDays = NSAttributedString(string: " ", attributes: attrs)
+ let sepMain = NSAttributedString(string: ":", attributes: attrs)
+ let sepFrames = NSAttributedString(string: frameRate.isDrop ? ";" : ":", attributes: attrs)
+ let sepSubFrames = NSAttributedString(string: ".", attributes: attrs)
+
+ #if os(macOS)
+ let invalidColor = invalidAttributes
+ ?? [NSAttributedString.Key.foregroundColor : NSColor.red]
+ #elseif os(iOS) || os(tvOS) || os(watchOS)
+ let invalidColor = invalidAttributes
+ ?? [NSAttributedString.Key.foregroundColor : UIColor.red]
+ #endif
+
+ let invalids = invalidComponents
+
+ let output = NSMutableAttributedString(string: "", attributes: attrs)
+
+ var piece: NSMutableAttributedString
+
+ // days
+ if days != 0 {
+ piece = NSMutableAttributedString(string: "\(days)", attributes: attrs)
+ if invalids.contains(.days) {
+ piece.addAttributes(invalidColor, range: NSRange(location: 0, length: piece.string.count))
+ }
+
+ output.append(piece)
+
+ output.append(sepDays)
+
+ }
+
+ // hours
+
+ piece = NSMutableAttributedString(string: String(format: "%02d", hours),
+ attributes: attrs)
+ if invalids.contains(.hours) {
+ piece.addAttributes(invalidColor, range: NSRange(location: 0, length: piece.string.count))
+ }
+
+ output.append(piece)
+
+ output.append(sepMain)
+
+ // minutes
+
+ piece = NSMutableAttributedString(string: String(format: "%02d", minutes),
+ attributes: attrs)
+ if invalids.contains(.minutes) {
+ piece.addAttributes(invalidColor, range: NSRange(location: 0, length: piece.string.count))
+ }
+
+ output.append(piece)
+
+ output.append(sepMain)
+
+ // seconds
+
+ piece = NSMutableAttributedString(string: String(format: "%02d", seconds),
+ attributes: attrs)
+ if invalids.contains(.seconds) {
+ piece.addAttributes(invalidColor, range: NSRange(location: 0, length: piece.string.count))
+ }
+
+ output.append(piece)
+
+ output.append(sepFrames)
+
+ // frames
+
+ piece = NSMutableAttributedString(string:
+ String(format: "%0\(frameRate.numberOfDigits)d", frames),
+ attributes: attrs)
+ if invalids.contains(.frames) {
+ piece.addAttributes(invalidColor, range: NSRange(location: 0, length: piece.string.count))
+ }
+
+ output.append(piece)
+
+ // subframes
+
+ if displaySubFrames {
+ let numberOfSubFramesDigits = validRange(of: .subFrames).upperBound.numberOfDigits
+
+ output.append(sepSubFrames)
+
+ piece = NSMutableAttributedString(string:
+ String(format: "%0\(numberOfSubFramesDigits)d", subFrames),
+ attributes: attrs)
+ if invalids.contains(.subFrames) {
+ piece.addAttributes(invalidColor, range: NSRange(location: 0, length: piece.string.count))
+ }
+
+ output.append(piece)
+ }
+
+ return output
+ }
+
+}
+
+
+// MARK: Setters
+
+extension OTTimecode {
+
+ /** Returns true/false depending on whether the string is formatted correctly or not.
+ Values which are out-of-bounds will be clamped to minimum or maximum possible values. (Clamping is based on the frame rate and `upperLimit` property.)
+ */
+ @discardableResult
+ public mutating func setTimecode(clamping string: String) -> Bool {
+ guard let tcVals = OTTimecode.decode(timecode: string) else { return false }
+
+ setTimecode(clamping: tcVals)
+
+ return true
+ }
+
+ /** Returns true/false depending on whether the string is formatted correctly or not.
+ Values which are out-of-bounds will also cause the setter to fail, and return false. (Validation is based on the frame rate and `upperLimit` property.)
+ */
+ @discardableResult
+ public mutating func setTimecode(exactly string: String) -> Bool {
+ guard let decoded = OTTimecode.decode(timecode: string) else { return false }
+
+ return setTimecode(exactly: decoded)
+ }
+
+ /** Returns true/false depending on whether the string is formatted correctly or not.
+ Values which are out-of-bounds will be clamped to minimum or maximum possible values. (Clamping is based on the frame rate and `upperLimit` property.)
+ */
+ @discardableResult
+ public mutating func setTimecode(wrapping string: String) -> Bool {
+ guard let tcVals = OTTimecode.decode(timecode: string) else { return false }
+
+ setTimecode(wrapping: tcVals)
+
+ return true
+ }
+
+ /** Returns true/false depending on whether the string is formatted correctly or not.
+ Timecode values will not be validated or rejected if they overflow.
+ */
+ @discardableResult
+ public mutating func setTimecode(rawValues string: String) -> Bool {
+ guard let tcVals = OTTimecode.decode(timecode: string) else { return false }
+
+ setTimecode(rawValues: tcVals)
+
+ return true
+ }
+
+}
+
+extension OTTimecode {
+
+ /** Decodes a Timecode string into its component values, without validating.
+
+ Returns nil only if the string is not formatted as expected; raw values themselves will be passed as-is.
+
+ Valid formats for 24-hour:
+
+ ```
+ "00:00:00:00" "00:00:00;00"
+ "00:00:00:00.00" "00:00:00;00.00"
+ ```
+
+ Valid formats for 100-day: All of the above, as well as:
+
+ ```
+ "0 00:00:00:00" "0 00:00:00;00"
+ "0:00:00:00:00" "0:00:00:00;00"
+ "0 00:00:00:00.00" "0 00:00:00;00.00"
+ "0:00:00:00:00.00" "0:00:00:00;00.00"
+ ```
+ */
+ public static func decode(timecode string: String) -> Components? {
+
+ let pattern = #"^(\d+)??[\:\s]??(\d+)[\:](\d+)[\:](\d+)[\:\;](\d+)[\.]??(\d+)??$"#
+
+ let matches = string.regexMatches(captureGroupsFromPattern: pattern)
+
+ // map Strings to Int
+
+ let ints = matches.map { $0 == nil ? nil : Int($0!) }
+
+ // basic sanity check - ensure there's at least 4 values
+
+ let nonNilCount = ints.filter { $0 != nil }.count
+
+ guard (4...6).contains(nonNilCount) else { return nil }
+
+ // return components
+
+ return Components(d: ints[0] ?? 0,
+ h: ints[1] ?? 0,
+ m: ints[2] ?? 0,
+ s: ints[3] ?? 0,
+ f: ints[4] ?? 0,
+ sf: ints[5] ?? 0)
+
+ // old code (pre-subframes)
+
+// var tcElements: [Substring] = ["","","","",""]
+//
+// // check for 8 digits with no separators
+// if string.count == 8 &&
+// CharacterSet.decimalDigits.isSuperset(of: CharacterSet(charactersIn: string)) {
+// // get units from 4 x 2 digit pairs
+// tcElements[0] = "00" // empty Days
+// tcElements[1] = string[string.startIndex...string.index(string.startIndex, offsetBy: 1)]
+// tcElements[2] = string[string.index(string.startIndex, offsetBy: 2)...string.index(string.startIndex, offsetBy: 3)]
+// tcElements[3] = string[string.index(string.startIndex, offsetBy: 4)...string.index(string.startIndex, offsetBy: 5)]
+// tcElements[4] = string[string.index(string.startIndex, offsetBy: 6)...string.index(string.startIndex, offsetBy: 7)]
+//
+// } else if 10...14 ~= string.count { // minimum place widths: 0:00:00:00
+// // attempt to split number components; not assuming they're numbers yet, if they can be converted to integers after this then that's validation enough
+// tcElements = string.split(whereSeparator: { $0 == ":" || $0 == ";" || $0 == "." })
+// guard 4...5 ~= tcElements.count else { return nil } // if days are separated by : ; or . then this will be 5
+//
+// if tcElements.count == 4 { // if 4, check to see if Days are included and separated by a space
+// let getDays = tcElements[0].split(separator: " ")
+// switch getDays.count {
+// case 1: // Days not present
+// tcElements = [Substring("00")] + tcElements // add empty Days buffer
+// case 2: // We can maybe assume there's Day and Hours
+// tcElements = [getDays[0]] + tcElements // add Days
+// tcElements[1] = getDays[1] // assign Hours
+// default: return nil
+// }
+// }
+//
+// } else {
+// // failed formatting validation
+// return nil
+//
+// }
+//
+// // ensure all values can be converted to a number; this will weed out any non-numerical elements
+// guard let d = Int(tcElements[0]),
+// let h = Int(tcElements[1]),
+// let m = Int(tcElements[2]),
+// let s = Int(tcElements[3]),
+// let f = Int(tcElements[4]) else { return nil }
+//
+// return Components(d: d, h: h, m: m, s: s, f: f)
+
+ }
+
+}
diff --git a/Sources/SwiftTimecode/Formatter/TextFormatter.swift b/Sources/SwiftTimecode/Formatter/TextFormatter.swift
new file mode 100644
index 00000000..6c5d5781
--- /dev/null
+++ b/Sources/SwiftTimecode/Formatter/TextFormatter.swift
@@ -0,0 +1,364 @@
+//
+// TextFormatter.swift
+// SwiftTimecode
+//
+// Created by Steffan Andrews on 2020-07-11.
+// Copyright © 2020 Steffan Andrews. All rights reserved.
+//
+
+import Foundation
+
+#if os(macOS)
+import Cocoa
+#elseif os(iOS) || os(tvOS) || os(watchOS)
+import UIKit
+#endif
+
+// MARK: - TextFormatter
+
+extension OTTimecode {
+
+ /// Formatter subclass
+ /// (Used in OTTimecode.TextField)
+ @objc(OTTimecodeTextFormatter)
+ public class TextFormatter: Formatter {
+
+ // MARK: properties
+
+ public var frameRate: OTTimecode.FrameRate?
+ public var upperLimit: OTTimecode.UpperLimit?
+ public var displaySubFrames: Bool?
+ public var subFramesDivisor: Int?
+
+ /// The formatter's `attributedString(...) -> NSAttributedString` output will override a control's alignment (ie: `NSTextField`).
+ /// Setting alignment here will add the appropriate paragraph alignment attribute to the output `NSAttributedString`.
+ public var alignment: NSTextAlignment = .natural
+
+ /// When set true, invalid timecode component values are individually attributed.
+ public var showsValidation: Bool = false
+
+ /// The `NSAttributedString` attributes applied to invalid values if `showsValidation` is set.
+ ///
+ /// Defaults to red foreground color.
+
+ public var validationAttributes: [NSAttributedString.Key : Any]
+ = {
+ #if os(macOS)
+ return [ .foregroundColor : NSColor.red ]
+ #elseif os(iOS) || os(tvOS) || os(watchOS)
+ return [ .foregroundColor : UIColor.red ]
+ #endif
+ }()
+
+
+ // MARK: init
+
+ public required init?(coder: NSCoder) {
+ super.init(coder: coder)
+ }
+
+ public init(frameRate: OTTimecode.FrameRate? = nil,
+ limit: OTTimecode.UpperLimit? = nil,
+ displaySubFrames: Bool? = nil,
+ subFramesDivisor: Int? = nil,
+ showsValidation: Bool = false,
+ validationAttributes: [NSAttributedString.Key : Any]? = nil)
+ {
+ super.init()
+
+ self.frameRate = frameRate
+ self.upperLimit = limit
+ self.subFramesDivisor = subFramesDivisor
+ self.displaySubFrames = displaySubFrames
+
+ self.showsValidation = showsValidation
+
+ if validationAttributes != nil {
+ self.validationAttributes = validationAttributes!
+ }
+ }
+
+ /// Initializes with properties from an `OTTimecode` object.
+ public convenience init(using timecode: OTTimecode,
+ showsValidation: Bool = false,
+ validationAttributes: [NSAttributedString.Key : Any]? = nil)
+ {
+ self.init(frameRate: timecode.frameRate,
+ limit: timecode.upperLimit,
+ displaySubFrames: timecode.displaySubFrames,
+ subFramesDivisor: timecode.subFramesDivisor,
+ showsValidation: showsValidation,
+ validationAttributes: validationAttributes)
+ }
+
+
+ public func inheritProperties(from other: OTTimecode.TextFormatter) {
+ self.frameRate = other.frameRate
+ self.upperLimit = other.upperLimit
+ self.subFramesDivisor = other.subFramesDivisor
+ self.displaySubFrames = other.displaySubFrames
+
+ self.alignment = other.alignment
+ self.showsValidation = other.showsValidation
+ self.validationAttributes = other.validationAttributes
+ }
+
+ // MARK: - Override methods
+
+
+
+ // MARK: string
+
+ public override func string(for obj: Any?) -> String? {
+
+ guard let string = obj as? String
+ else { return nil }
+
+ guard var tc = timecodeWithProperties
+ else { return string }
+
+ // form timecode components without validating
+ guard let tcc = OTTimecode.decode(timecode: string)
+ else { return string }
+
+ // set values without validating
+ tc.setTimecode(rawValues: tcc)
+
+ return tc.stringValue
+
+ }
+
+
+ // MARK: attributedString
+
+ public override func attributedString(for obj: Any,
+ withDefaultAttributes attrs: [NSAttributedString.Key : Any]? = nil) -> NSAttributedString? {
+
+ guard let string = string(for: obj)
+ else { return nil }
+
+ func entirelyInvalid() -> NSAttributedString {
+ return showsValidation
+ ? NSAttributedString(string: string,
+ attributes: validationAttributes
+ .merging(attrs ?? [:], uniquingKeysWith: { (current, _) in current }))
+ .addingAttribute(alignment: self.alignment)
+ : NSAttributedString(string: string, attributes: attrs)
+ .addingAttribute(alignment: self.alignment)
+ }
+
+ // grab properties from the formatter
+ guard var tc = timecodeWithProperties
+ else { return entirelyInvalid() }
+
+ // form timecode components without validating
+ guard let tcc = OTTimecode.decode(timecode: string)
+ else { return entirelyInvalid() }
+
+ // set values without validating
+ tc.setTimecode(rawValues: tcc)
+
+ return
+ (
+ showsValidation
+ ? tc.stringValueValidated(invalidAttributes: validationAttributes,
+ withDefaultAttributes: attrs)
+ : NSAttributedString(string: string, attributes: attrs)
+ )
+ .addingAttribute(alignment: self.alignment)
+
+ }
+
+ public override func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer?,
+ for string: String,
+ errorDescription error: AutoreleasingUnsafeMutablePointer?) -> Bool
+ {
+ obj?.pointee = string as NSString
+ return true
+ }
+
+
+ // MARK: isPartialStringValid
+
+ public override func isPartialStringValid(_ partialStringPtr: AutoreleasingUnsafeMutablePointer,
+ proposedSelectedRange proposedSelRangePtr: NSRangePointer?,
+ originalString origString: String,
+ originalSelectedRange origSelRange: NSRange,
+ errorDescription error: AutoreleasingUnsafeMutablePointer?) -> Bool
+ {
+ guard let frameRate = frameRate else { return true }
+ guard let limit = upperLimit else { return true }
+ //guard let subFramesDivisor = subFramesDivisor else { return true }
+ guard let displaySubFrames = displaySubFrames else { return true }
+
+ let partialString = partialStringPtr.pointee as String
+
+ // sanity checks
+
+ if partialString.isEmpty { return true } // allow empty field
+ //if partialString.count > 20 { return false } // don't allow too many chars
+
+ // constants
+
+ let numberChars = CharacterSet(charactersIn: "0123456789")
+ //let coreSeparatorChars = CharacterSet(charactersIn: ":;")
+ //let allSeparatorChars = CharacterSet(charactersIn: ":;. ")
+
+ let allowedChars = CharacterSet(charactersIn: "0123456789:;. ")
+ let disallowedChars = allowedChars.inverted
+
+ // more sanity checks
+
+ if let _ = partialString.rangeOfCharacter(from: disallowedChars,
+ options: .caseInsensitive)
+ {
+ error?.pointee = NSString("Invalid characters.")
+ return false
+ }
+
+ // parse
+
+ var string = ""
+ var fixed = false
+
+ var consecutiveIntCount = 0
+ var intGrouping = 0
+ var spaceCount = 0
+ var colonCount = 0
+ var periodCount = 0
+ var lastChar: Character? = nil
+
+ for var char in partialString {
+
+ // prep
+
+ let originalChar = char ; _ = originalChar
+
+ if numberChars.contains(char)
+ { consecutiveIntCount += 1 }
+
+ // separators
+
+ switch char {
+ case ".":
+ if colonCount < 3 {
+ char = frameRate.isDrop && (colonCount == 2)
+ ? ";" : ":"
+
+ fixed = true
+ }
+ else if periodCount == 0 { break }
+ else { return false }
+
+ case ";":
+ if colonCount < 3 {
+ char = frameRate.isDrop && (colonCount == 2)
+ ? ";" : ":"
+
+ fixed = true
+ }
+
+ default: break
+ }
+
+ if char == " " {
+ if limit == ._24hours
+ { return false }
+
+ if !(intGrouping == 1 && spaceCount == 0 && colonCount == 0 && periodCount == 0)
+ { return false }
+
+ spaceCount += 1
+ }
+
+ // // don't allow two separators in a row
+ //
+ // if allSeparatorChars.containsUnicodeScalars(of: char),
+ // lastChar != nil,
+ // allSeparatorChars.containsUnicodeScalars(of: lastChar!)
+ // { return false }
+
+ // separator validation
+
+ if (char == ":" || char == ";")
+ { colonCount += 1 ; consecutiveIntCount = 0 }
+
+ if (char == ":" || char == ";") && colonCount >= 4
+ { return false }
+
+ // period validation
+
+ if char == "."
+ { periodCount += 1 }
+
+ if char == "." && periodCount > 1
+ { return false }
+
+ if char == "." && !displaySubFrames
+ { return false }
+
+ // number validation (?)
+
+
+ // cleanup
+
+ if numberChars.contains(char) {
+ if lastChar != nil {
+ if !numberChars.contains(lastChar!)
+ { intGrouping += 1 }
+ } else {
+ intGrouping += 1
+ }
+ }
+
+ // cycle variables
+
+ lastChar = char
+
+ // append char
+
+ string += "\(char)"
+
+ }
+
+ if fixed {
+
+ partialStringPtr.pointee = NSString(string: string)
+
+ return false
+
+ } else {
+
+ return true
+
+ }
+
+ }
+
+ }
+
+}
+
+
+// MARK: timecodeWithProperties
+
+extension OTTimecode.TextFormatter {
+
+ public var timecodeWithProperties: OTTimecode? {
+
+ guard let frameRate = frameRate else { return nil }
+ guard let upperLimit = upperLimit else { return nil }
+ guard let subFramesDivisor = subFramesDivisor else { return nil }
+ guard let displaySubFrames = displaySubFrames else { return nil }
+
+ var tc = OTTimecode(at: frameRate,
+ limit: upperLimit,
+ subFramesDivisor: subFramesDivisor)
+
+ tc.displaySubFrames = displaySubFrames
+
+ return tc
+
+ }
+
+}
diff --git a/Sources/SwiftTimecode/FrameRate/FrameRate Properties.swift b/Sources/SwiftTimecode/FrameRate/FrameRate Properties.swift
new file mode 100644
index 00000000..e01d1fdb
--- /dev/null
+++ b/Sources/SwiftTimecode/FrameRate/FrameRate Properties.swift
@@ -0,0 +1,303 @@
+//
+// FrameRate Properties.swift
+// SwiftTimecode
+//
+// Created by Steffan Andrews on 2020-06-15.
+// Copyright © 2020 Steffan Andrews. All rights reserved.
+//
+
+import Foundation
+
+
+// MARK: stringValue ...
+
+extension OTTimecode.FrameRate {
+
+ /// Returns human-readable frame rate string.
+ public var stringValue: String {
+ switch self {
+ case ._23_976: return "23.976 NDF"
+ case ._24: return "24 NDF"
+ case ._24_98: return "24.98 NDF"
+ case ._25: return "25 NDF"
+ case ._29_97: return "29.97 NDF"
+ case ._29_97_drop: return "29.97 DF"
+ case ._30: return "30 NDF"
+ case ._30_drop: return "30 DF"
+ case ._47_952: return "47.952 NDF"
+ case ._48: return "48 NDF"
+ case ._50: return "50 NDF"
+ case ._59_94: return "59.94 NDF"
+ case ._59_94_drop: return "59.94 DF"
+ case ._60: return "60 NDF"
+ case ._60_drop: return "60 DF"
+ case ._100: return "100 NDF"
+ case ._119_88: return "119.88 NDF"
+ case ._119_88_drop: return "119.88 DF"
+ case ._120: return "120 NDF"
+ case ._120_drop: return "120 DF"
+ }
+ }
+
+ /// Initializes from a `stringValue` string. Case-sensitive.
+ public init?(stringValue: String) {
+ if let findMatch = Self.allCases.first(where: { $0.stringValue == stringValue }) {
+ self = findMatch
+ } else {
+ return nil
+ }
+ }
+
+ /// Returns human-readable frame rate string, ie: suitable for use in filenames.
+ public var stringValueFileNameCompatible: String {
+ switch self {
+ case ._23_976: return "23_976NDF"
+ case ._24: return "24NDF"
+ case ._24_98: return "24_98DNF"
+ case ._25: return "25NDF"
+ case ._29_97: return "29_97NDF"
+ case ._29_97_drop: return "29_97DF"
+ case ._30: return "30NDF"
+ case ._30_drop: return "30DF"
+ case ._47_952: return "47_952NDF"
+ case ._48: return "48NDF"
+ case ._50: return "50NDF"
+ case ._59_94: return "59_94NDF"
+ case ._59_94_drop: return "59_94DF"
+ case ._60: return "60NDF"
+ case ._60_drop: return "60DF"
+ case ._100: return "100NDF"
+ case ._119_88: return "119_88NDF"
+ case ._119_88_drop: return "119_88DF"
+ case ._120: return "120NDF"
+ case ._120_drop: return "120DF"
+ }
+ }
+
+}
+
+
+// MARK: Public meta properties
+
+extension OTTimecode.FrameRate {
+
+ /// Returns true if frame rate is drop-frame.
+ public var isDrop: Bool {
+ switch self {
+ case ._23_976: return false
+ case ._24: return false
+ case ._24_98: return false
+ case ._25: return false
+ case ._29_97: return false
+ case ._29_97_drop: return true
+ case ._30: return false
+ case ._30_drop: return true
+ case ._47_952: return false
+ case ._48: return false
+ case ._50: return false
+ case ._59_94: return false
+ case ._59_94_drop: return true
+ case ._60: return false
+ case ._60_drop: return true
+ case ._100: return false
+ case ._119_88: return false
+ case ._119_88_drop: return true
+ case ._120: return false
+ case ._120_drop: return true
+ }
+ }
+
+ /// Total number of elapsed frames that comprise 1 second of timecode.
+ public var maxFrames: Int {
+ switch self {
+ case ._23_976: return 24
+ case ._24: return 24
+ case ._24_98: return 25
+ case ._25: return 25
+ case ._29_97: return 30
+ case ._29_97_drop: return 30
+ case ._30: return 30
+ case ._30_drop: return 30
+ case ._47_952: return 48
+ case ._48: return 48
+ case ._50: return 50
+ case ._59_94: return 60
+ case ._59_94_drop: return 60
+ case ._60: return 60
+ case ._60_drop: return 60
+ case ._100: return 100
+ case ._119_88: return 120
+ case ._119_88_drop: return 120
+ case ._120: return 120
+ case ._120_drop: return 120
+ }
+ }
+
+ /// Max frame number displayable before seconds roll over.
+ public var maxFrameNumberDisplayable: Int {
+ return maxFrames - 1
+ }
+
+ /// Returns max elapsed frames from 0 to and including rolling over to `extent`.
+ public func maxTotalFrames(in extent: OTTimecode.UpperLimit) -> Int {
+ // template to calculate:
+ // Int(Double(extent.maxDays) * 24 * 60 * 60 * self.frameRateForCalculation)
+
+ switch extent {
+ case ._24hours:
+ switch self {
+ case ._23_976: return 2073600 // @ 24hours
+ case ._24: return 2073600 // @ 24hours
+ case ._24_98: return 2160000 // @ 24hours
+ case ._25: return 2160000 // @ 24hours
+ case ._29_97: return 2592000 // @ 24hours
+ case ._29_97_drop: return 2589408 // @ 24hours
+ case ._30: return 2592000 // @ 24hours
+ case ._30_drop: return 2589408 // @ 24hours
+ case ._47_952: return 4147200 // @ 24hours
+ case ._48: return 4147200 // @ 24hours
+ case ._50: return 4320000 // @ 24hours
+ case ._59_94: return 5184000 // @ 24hours (_29_97 * 2 in theory)
+ case ._59_94_drop: return 5178816 // @ 24hours (_29_97_drop * 2, in theory)
+ case ._60: return 5184000 // @ 24hours
+ case ._60_drop: return 5178816 // @ 24hours
+ case ._100: return 8640000 // @ 24hours
+ case ._119_88: return 10368000 // @ 24hours (_29_97 * 4 in theory)
+ case ._119_88_drop: return 10357632 // @ 24hours (_29_97_drop * 4, in theory)
+ case ._120: return 10368000 // @ 24hours
+ case ._120_drop: return 10357632 // @ 24hours
+ }
+
+ case ._100days:
+ return self.maxTotalFrames(in: ._24hours) * extent.maxDays
+ }
+
+ }
+
+ /// Returns max elapsed frames possible before rolling over to 0.
+ /// (Number of frames from 0 to `extent` minus one subframe).
+ public func maxTotalFramesExpressible(in extent: OTTimecode.UpperLimit) -> Int {
+ return maxTotalFrames(in: extent) - 1
+ }
+
+}
+
+
+// MARK: Internal properties
+
+extension OTTimecode.FrameRate {
+
+ /// Internal use.
+ internal var frameRateForElapsedFramesCalculation: Double {
+ switch self {
+ case ._23_976: return 24.0
+ case ._24: return 24.0
+ case ._24_98: return 25.0
+ case ._25: return 25.0
+ case ._29_97: return 30.0
+ case ._29_97_drop: return 29.97 // used in special drop-frame calculation
+ case ._30: return 30.0
+ case ._30_drop: return 29.97
+ case ._47_952: return 48.0
+ case ._48: return 48.0
+ case ._50: return 50.0
+ case ._59_94: return 60.0
+ case ._59_94_drop: return 59.94 // used in special drop-frame calculation
+ case ._60: return 60.0
+ case ._60_drop: return 59.94 // used in special drop-frame calculation
+ case ._100: return 100.0
+ case ._119_88: return 120.0
+ case ._119_88_drop: return 119.88 // used in special drop-frame calculation
+ case ._120: return 120.0
+ case ._120_drop: return 119.88 // used in special drop-frame calculation
+ }
+ }
+
+ /// Internal use. Used in marker MIDI file export.
+ internal var frameRateForRealTimeCalculation: Double {
+ switch self {
+ case ._23_976: return 24.0 / 1.001 // confirmed correct
+ case ._24: return 24.0 // confirmed correct
+ case ._24_98: return 25.0 / 1.001
+ case ._25: return 25.0 // confirmed correct
+ case ._29_97: return 30.0 / 1.001 // confirmed correct
+ case ._29_97_drop: return 30.0 / 1.001 // confirmed correct
+ case ._30: return 30.0 // confirmed correct
+ case ._30_drop: return 30.0 / 1.001
+ case ._47_952: return 48.0 / 1.001
+ case ._48: return 48.0
+ case ._50: return 50.0 // confirmed correct
+ case ._59_94: return 60.0 / 1.001 // confirmed correct
+ case ._59_94_drop: return 60.0 / 1.001 // confirmed correct
+ case ._60: return 60.0 // confirmed correct
+ case ._60_drop: return 60.0 / 1.001
+ case ._100: return 100.0
+ case ._119_88: return 120.0 / 1.001
+ case ._119_88_drop: return 120.0 / 1.001
+ case ._120: return 120.0
+ case ._120_drop: return 120.0 / 1.001
+ }
+ }
+
+ /// Internal use.
+ internal var framesDroppedPerMinute: Double {
+ switch self {
+ case ._29_97_drop: return 2.0
+ case ._30_drop: return 2.0
+ case ._59_94_drop: return 4.0
+ case ._60_drop: return 4.0
+ case ._119_88_drop: return 8.0
+ case ._120_drop: return 8.0
+
+ case ._23_976,
+ ._24,
+ ._24_98,
+ ._25,
+ ._29_97,
+ ._30,
+ ._47_952,
+ ._48,
+ ._50,
+ ._59_94,
+ ._60,
+ ._100,
+ ._119_88,
+ ._120:
+
+ // this value is not actually used
+ // this is only here so that when adding frame rates to the framework, the compiler will throw an error to remind you to add the enum case here
+ return 0.0
+
+ }
+
+ }
+
+ /// Returns the number of digits required for frames within the timecode string.
+ ///
+ /// ie: 24 fps would return 2, but 120 fps would return 3.
+ public var numberOfDigits: Int {
+ switch self {
+ case ._23_976: return 2
+ case ._24: return 2
+ case ._24_98: return 2
+ case ._25: return 2
+ case ._29_97: return 2
+ case ._29_97_drop: return 2
+ case ._30: return 2
+ case ._30_drop: return 2
+ case ._47_952: return 2
+ case ._48: return 2
+ case ._50: return 2
+ case ._59_94: return 2
+ case ._59_94_drop: return 2
+ case ._60: return 2
+ case ._60_drop: return 2
+ case ._100: return 3
+ case ._119_88: return 3
+ case ._119_88_drop: return 3
+ case ._120: return 3
+ case ._120_drop: return 3
+ }
+ }
+
+}
diff --git a/Sources/SwiftTimecode/FrameRate/FrameRate String Extensions.swift b/Sources/SwiftTimecode/FrameRate/FrameRate String Extensions.swift
new file mode 100644
index 00000000..93e01c69
--- /dev/null
+++ b/Sources/SwiftTimecode/FrameRate/FrameRate String Extensions.swift
@@ -0,0 +1,20 @@
+//
+// FrameRate String Extensions.swift
+// SwiftTimecode
+//
+// Created by Steffan Andrews on 2020-08-18.
+// Copyright © 2020 Steffan Andrews. All rights reserved.
+//
+
+import Foundation
+
+extension String {
+
+ /// Convenience method to call `OTTimecode.FrameRate(stringValue: self)`
+ public var toFrameRate: OTTimecode.FrameRate? {
+
+ OTTimecode.FrameRate(stringValue: self)
+
+ }
+
+}
diff --git a/Sources/SwiftTimecode/FrameRate/FrameRate.swift b/Sources/SwiftTimecode/FrameRate/FrameRate.swift
new file mode 100644
index 00000000..b255a295
--- /dev/null
+++ b/Sources/SwiftTimecode/FrameRate/FrameRate.swift
@@ -0,0 +1,135 @@
+//
+// FrameRate.swift
+// SwiftTimecode
+//
+// Created by Steffan Andrews on 2018-07-20.
+// Copyright © 2018 Steffan Andrews. All rights reserved.
+//
+
+import Foundation
+
+extension OTTimecode {
+
+ // MARK: - FrameRate
+
+ public enum FrameRate: Int {
+
+ /// 23.976 fps (aka 23.98)
+ ///
+ /// Also known as 24p for HD video, sometimes rounded up to 23.98 fps. started out as the format for dealing with 24fps film in a NTSC post environment.
+ case _23_976
+
+ /// 24 fps
+ ///
+ /// (film, ATSC, 2k, 4k, 6k)
+ case _24
+
+ /// 24.98 fps
+ ///
+ /// This frame rate is commonly used to facilitate transfers between PAL and NTSC video and film sources. It is mostly used to compensate for some error.
+ case _24_98
+
+ /// 25 fps
+ ///
+ /// (PAL, used in Europe, Uruguay, Argentina, Australia), SECAM, DVB, ATSC)
+ case _25
+
+ /// 29.97 fps (30p)
+ ///
+ /// (NTSC American System (US, Canada, Mexico, Colombia, etc.), ATSC, PAL-M (Brazil))
+ /// (30 / 1.001) frame/sec
+ case _29_97
+
+ /// 29.97 drop fps
+ case _29_97_drop
+
+ /// 30 fps
+ ///
+ /// (ATSC) This is the frame count of NTSC broadcast video. However, the actual frame rate or speed of the video format runs at 29.97 fps.
+ ///
+ /// This timecode clock does not run in realtime. It is slightly slower by 0.1%.
+ /// ie: 1:00:00:00:00 at 30 fps is approx 1:00:00:00;02 in 29.97df
+ case _30
+
+ /// 30 drop fps:
+ ///
+ /// The 30 fps drop-frame count is an adaptation that allows a timecode display running at 29.97 fps to actually show the clock-on-the-wall-time of the timeline by “dropping” or skipping specific frame numbers in order to “catch the clock up” to realtime.
+ case _30_drop
+
+ /// 47.952 (48p?)
+ ///
+ /// Double 23.976 fps
+ case _47_952
+
+ /// 48 fps
+ ///
+ /// Double 24 fps
+ case _48
+
+ /// 50 fps
+ ///
+ /// Double 25 fps
+ case _50
+
+ /// 59.94 fps
+ ///
+ /// Double 29.97 fps
+ ///
+ /// This video frame rate is supported by high definition cameras and is compatible with NTSC (29.97 fps).
+ case _59_94
+
+ /// 59.94 drop fps
+ ///
+ /// Double 29.97 drop fps
+ case _59_94_drop
+
+ /// 60 fps
+ ///
+ /// Double 30 fps
+ ///
+ /// This video frame rate is supported by many high definition cameras. However, the NTSC compatible 59.94 fps frame rate is much more common.
+ case _60
+
+ /// 60 drop fps
+ ///
+ /// Double 30 fps
+ case _60_drop
+
+ /// 100 fps
+ ///
+ /// Double 50 fps / quadruple 25 fps
+ case _100
+
+ /// 119.88 fps
+ ///
+ /// Double 59.94 fps / quadruple 29.97 fps
+ case _119_88
+
+ /// 119.88 drop fps
+ ///
+ /// Double 59.94 drop fps / quadruple 29.97 drop fps
+ case _119_88_drop
+
+ /// 120 fps
+ ///
+ /// Double 60 fps / quadruple 30 fps
+ case _120
+
+ /// 120 drop fps
+ ///
+ /// Double 60 fps drop / quadruple 30 fps drop
+ case _120_drop
+
+ }
+
+}
+
+extension OTTimecode.FrameRate: CaseIterable {
+
+ /// All dropframe frame rates.
+ public static let allDrop: [Self] = allCases.filter { $0.isDrop }
+
+ /// All non-dropframe frame rates.
+ public static let allNonDrop: [Self] = allCases.filter { !$0.isDrop }
+
+}
diff --git a/Sources/SwiftTimecode/Math/OTTimecode Math Internal.swift b/Sources/SwiftTimecode/Math/OTTimecode Math Internal.swift
new file mode 100644
index 00000000..5bcc6f19
--- /dev/null
+++ b/Sources/SwiftTimecode/Math/OTTimecode Math Internal.swift
@@ -0,0 +1,270 @@
+//
+// OTTimecode Math Internal.swift
+// SwiftTimecode
+//
+// Created by Steffan Andrews on 2020-06-15.
+// Copyright © 2020 Steffan Andrews. All rights reserved.
+//
+
+import Foundation
+
+extension OTTimecode {
+
+ // MARK: - Add
+
+ /// Utility function to add a duration to a base timecode. Returns nil if it overflows possible timecode values.
+ internal func __add(exactly duration: Components, to base: Components) -> Components? {
+
+ let tcOrigin = Self.totalElapsedFrames(of: base,
+ at: frameRate,
+ subFramesDivisor: subFramesDivisor)
+ let tcAdd = Self.totalElapsedFrames(of: duration,
+ at: frameRate,
+ subFramesDivisor: subFramesDivisor)
+ let tcNew = tcOrigin + tcAdd
+
+ if tcNew > maxFramesAndSubframesExpressible { return nil }
+
+ return Self.components(from: tcNew,
+ at: frameRate,
+ subFramesDivisor: subFramesDivisor)
+
+ }
+
+ /// Utility function to add a duration to a base timecode. Clamps to maximum timecode expressible.
+ internal func __add(clamping duration: Components, to base: Components) -> Components {
+
+ let tcOrigin = Self.totalElapsedFrames(of: base,
+ at: frameRate,
+ subFramesDivisor: subFramesDivisor)
+ let tcAdd = Self.totalElapsedFrames(of: duration,
+ at: frameRate,
+ subFramesDivisor: subFramesDivisor)
+ var tcNew = tcOrigin + tcAdd
+
+ tcNew = tcNew.clamped(to: 0.0...maxFramesAndSubframesExpressible)
+
+ return Self.components(from: tcNew,
+ at: frameRate,
+ subFramesDivisor: subFramesDivisor)
+
+ }
+
+ /// Utility function to add a duration to a base timecode. Wraps around the clock as set by the `upperLimit` property.
+ internal func __add(wrapping duration: Components, to base: Components) -> Components {
+
+ let tcOrigin = Self.totalElapsedFrames(of: base,
+ at: frameRate,
+ subFramesDivisor: subFramesDivisor)
+ let tcAdd = Self.totalElapsedFrames(of: duration,
+ at: frameRate,
+ subFramesDivisor: subFramesDivisor)
+ var tcNew = tcOrigin + tcAdd
+
+ let wrapTest = tcNew.quotientAndRemainder(dividingBy: Double(frameRate.maxTotalFrames(in: upperLimit)))
+
+ // check for a negative result and wrap accordingly
+ if tcNew < 0.0 {
+ tcNew = Double(frameRate.maxTotalFrames(in: upperLimit)) + wrapTest.remainder // wrap around
+ } else {
+ tcNew = wrapTest.remainder
+ }
+
+ // TODO: - ***** can implement later: number of times the value wrapped will be stored in wrapTest.quotient
+
+ return Self.components(from: tcNew,
+ at: frameRate,
+ subFramesDivisor: subFramesDivisor)
+
+ }
+
+
+ // MARK: - Subtract
+
+ /// Utility function to add a duration to a base timecode. Returns nil if overflows possible timecode values.
+ internal func __subtract(exactly duration: Components, from base: Components) -> Components? {
+
+ let tcOrigin = Self.totalElapsedFrames(of: base,
+ at: frameRate,
+ subFramesDivisor: subFramesDivisor)
+ let tcSubtract = Self.totalElapsedFrames(of: duration,
+ at: frameRate,
+ subFramesDivisor: subFramesDivisor)
+ let tcNew = tcOrigin - tcSubtract
+
+ if tcNew < 0 { return nil }
+
+ return Self.components(from: tcNew,
+ at: frameRate,
+ subFramesDivisor: subFramesDivisor)
+
+ }
+
+ /// Utility function to add a duration to a base timecode. Clamps to maximum timecode expressible.
+ internal func __subtract(clamping duration: Components, from base: Components) -> Components {
+
+ let tcOrigin = Self.totalElapsedFrames(of: base,
+ at: frameRate,
+ subFramesDivisor: subFramesDivisor)
+ let tcSubtract = Self.totalElapsedFrames(of: duration,
+ at: frameRate,
+ subFramesDivisor: subFramesDivisor)
+ let tcNew = tcOrigin - tcSubtract
+
+ if tcNew < 0 { return Components(d: 0, h: 0, m: 0, s: 0, f: 0) }
+
+ return Self.components(from: tcNew,
+ at: frameRate,
+ subFramesDivisor: subFramesDivisor)
+
+ }
+
+ /// Utility function to add a duration to a base timecode. Wraps around the clock as set by the `upperLimit` property.
+ internal func __subtract(wrapping duration: Components, from base: Components) -> Components {
+
+ let tcOrigin = Self.totalElapsedFrames(of: base,
+ at: frameRate,
+ subFramesDivisor: subFramesDivisor)
+ let tcSubtract = Self.totalElapsedFrames(of: duration,
+ at: frameRate,
+ subFramesDivisor: subFramesDivisor)
+ var tcNew = tcOrigin - tcSubtract
+
+ // TODO: - ***** can implement later: also return number of times the value wrapped
+
+ let wrapTest = tcNew.quotientAndRemainder(dividingBy: Double(frameRate.maxTotalFrames(in: upperLimit)))
+
+ if tcNew < 0 {
+ tcNew = Double(frameRate.maxTotalFrames(in: upperLimit)) + wrapTest.remainder // wrap around
+ return Self.components(from: tcNew,
+ at: frameRate,
+ subFramesDivisor: subFramesDivisor)
+ } else {
+ return Self.components(from: wrapTest.remainder,
+ at: frameRate,
+ subFramesDivisor: subFramesDivisor)
+ }
+
+ }
+
+
+ // MARK: - Multiply
+
+ /// Utility function to multiply a base timecode by a duration. Returns nil if it overflows possible timecode values.
+ internal func __multiply(exactly duration: Double, with: Components) -> Components? {
+
+ let tcOrigin = Double(Self.totalElapsedFrames(of: with,
+ at: frameRate,
+ subFramesDivisor: subFramesDivisor))
+ let tcNew = Int(tcOrigin * duration)
+
+ if tcNew > frameRate.maxTotalFramesExpressible(in: upperLimit) { return nil }
+
+ return Self.components(from: tcNew,
+ at: frameRate,
+ subFramesDivisor: subFramesDivisor)
+
+ }
+
+ /// Utility function to multiply a base timecode by a duration. Clamps to maximum timecode expressible.
+ internal func __multiply(clamping duration: Double, with: Components) -> Components {
+
+ let tcOrigin = Double(Self.totalElapsedFrames(of: with,
+ at: frameRate,
+ subFramesDivisor: subFramesDivisor))
+ var tcNew = Int(tcOrigin * duration)
+
+ tcNew = tcNew.clamped(to: 0...frameRate.maxTotalFramesExpressible(in: upperLimit))
+
+ return Self.components(from: tcNew,
+ at: frameRate,
+ subFramesDivisor: subFramesDivisor)
+
+ }
+
+ /// Utility function to multiply a base timecode by a duration. Wraps around the clock as set by the `upperLimit` property.
+ internal func __multiply(wrapping duration: Double, with: Components) -> Components {
+
+ let tcOrigin = Double(Self.totalElapsedFrames(of: with,
+ at: frameRate,
+ subFramesDivisor: subFramesDivisor))
+ var tcNew = Int(tcOrigin * duration)
+
+ let wrapTest = tcNew.quotientAndRemainder(dividingBy: frameRate.maxTotalFrames(in: upperLimit))
+
+ // check for a negative result and wrap accordingly
+ if tcNew < 0 {
+ tcNew = frameRate.maxTotalFrames(in: upperLimit) + wrapTest.remainder // wrap around
+ } else {
+ tcNew = wrapTest.remainder
+ }
+
+ // TODO: - ***** can implement later: number of times the value wrapped will be stored in wrapTest.quotient
+
+ return Self.components(from: tcNew,
+ at: frameRate,
+ subFramesDivisor: subFramesDivisor)
+
+ }
+
+
+ // MARK: - Divide
+
+ /// Utility function to divide a base timecode by a duration. Returns nil if it overflows possible timecode values.
+ internal func __divide(exactly duration: Double, into: Components) -> Components? {
+
+ let tcOrigin = Double(Self.totalElapsedFrames(of: into,
+ at: frameRate,
+ subFramesDivisor: subFramesDivisor))
+ let tcNew = Int(tcOrigin / duration)
+
+ if tcNew > frameRate.maxTotalFramesExpressible(in: upperLimit) { return nil }
+
+ return Self.components(from: tcNew,
+ at: frameRate,
+ subFramesDivisor: subFramesDivisor)
+
+ }
+
+ /// Utility function to divide a base timecode by a duration. Clamps to maximum timecode expressible.
+ internal func __divide(clamping duration: Double, into: Components) -> Components {
+
+ let tcOrigin = Double(Self.totalElapsedFrames(of: into,
+ at: frameRate,
+ subFramesDivisor: subFramesDivisor))
+ var tcNew = Int(tcOrigin / duration)
+
+ tcNew = tcNew.clamped(to: 0...frameRate.maxTotalFramesExpressible(in: upperLimit))
+
+ return Self.components(from: tcNew,
+ at: frameRate,
+ subFramesDivisor: subFramesDivisor)
+
+ }
+
+ /// Utility function to divide a base timecode by a duration. Wraps around the clock as set by the `upperLimit` property.
+ internal func __divide(wrapping duration: Double, into: Components) -> Components {
+
+ let tcOrigin = Double(Self.totalElapsedFrames(of: into,
+ at: frameRate,
+ subFramesDivisor: subFramesDivisor))
+ var tcNew = Int(tcOrigin / duration)
+
+ let wrapTest = tcNew.quotientAndRemainder(dividingBy: frameRate.maxTotalFrames(in: upperLimit))
+
+ // check for a negative result and wrap accordingly
+ if tcNew < 0 {
+ tcNew = frameRate.maxTotalFrames(in: upperLimit) + wrapTest.remainder // wrap around
+ } else {
+ tcNew = wrapTest.remainder
+ }
+
+ // TODO: - ***** can implement later: number of times the value wrapped will be stored in wrapTest.quotient
+
+ return Self.components(from: tcNew,
+ at: frameRate,
+ subFramesDivisor: subFramesDivisor)
+
+ }
+
+}
diff --git a/Sources/SwiftTimecode/Math/OTTimecode Math Public.swift b/Sources/SwiftTimecode/Math/OTTimecode Math Public.swift
new file mode 100644
index 00000000..efc77794
--- /dev/null
+++ b/Sources/SwiftTimecode/Math/OTTimecode Math Public.swift
@@ -0,0 +1,270 @@
+//
+// OTTimecode Math Public.swift
+// SwiftTimecode
+//
+// Created by Steffan Andrews on 2018-07-20.
+// Copyright © 2018 Steffan Andrews. All rights reserved.
+//
+
+import Foundation
+
+extension OTTimecode {
+
+ // MARK: - Add
+
+ /// Add a duration to the current timecode.
+ /// Returns false if resulting value is not within valid timecode range.
+ /// Input values can be as large as desired and will be calculated recursively. ie: (0,0,0,1000) or (0,0,500,0)
+ public mutating func add(_ exactly: Components) -> Bool {
+ guard let newTC = __add(exactly: exactly, to: components) else { return false }
+
+ return setTimecode(exactly: newTC)
+ }
+
+ /// Add a duration to the current timecode.
+ /// Clamps to valid timecodes.
+ /// Input values can be as large as desired and will be calculated recursively. ie: (0,0,0,1000) or (0,0,500,0)
+ public mutating func add(clamping values: Components) {
+ let newTC = __add(clamping: values, to: components)
+
+ _ = setTimecode(exactly: newTC) // guaranteed to work
+ }
+
+ /// Add a duration to the current timecode.
+ /// Wraps around the clock as set by the `upperLimit` property.
+ /// Input values can be as large as desired and will be calculated recursively. ie: (0,0,0,1000) or (0,0,500,0)
+ public mutating func add(wrapping values: Components) {
+ let newTC = __add(wrapping: values, to: components)
+
+ _ = setTimecode(exactly: newTC) // guaranteed to work
+ }
+
+ /// Add a duration to the current timecode and return a new instance with the new timecode.
+ /// Returns nil if resulting value is not within valid timecode range.
+ /// Input values can be as large as desired and will be calculated recursively. ie: (0,0,0,1000) or (0,0,500,0)
+ public func adding(_ exactly: Components) -> OTTimecode? {
+ guard let newTC = __add(exactly: exactly, to: components) else { return nil }
+
+ var newOTTimecode = self // copy self
+ guard newOTTimecode.setTimecode(exactly: newTC) else { return nil }
+
+ return newOTTimecode
+ }
+
+ /// Add a duration to the current timecode and return a new instance with the new timecode.
+ /// Clamps to valid timecodes as set by the `upperLimit` property.
+ /// Input values can be as large as desired and will be calculated recursively. ie: (0,0,0,1000) or (0,0,500,0)
+ public func adding(clamping values: Components) -> OTTimecode {
+ let newTC = __add(clamping: values, to: components)
+
+ var newOTTimecode = self // copy self
+ _ = newOTTimecode.setTimecode(exactly: newTC) // guaranteed to work
+
+ return newOTTimecode
+ }
+
+ /// Add a duration to the current timecode and return a new instance with the new timecode.
+ /// Wraps around the clock as set by the `upperLimit` property.
+ /// Input values can be as large as desired and will be calculated recursively. ie: (0,0,0,1000) or (0,0,500,0)
+ public func adding(wrapping values: Components) -> OTTimecode {
+ let newTC = __add(wrapping: values, to: components)
+
+ var newOTTimecode = self // copy self
+ _ = newOTTimecode.setTimecode(exactly: newTC) // guaranteed to work
+
+ return newOTTimecode
+ }
+
+
+ // MARK: - Subtract
+
+ /// Subtract a duration from the current timecode.
+ /// Returns false if resulting value is not within valid timecode range.
+ /// Input values can be as large as desired and will be calculated recursively. ie: (0,0,0,1000) or (0,0,500,0)
+ public mutating func subtract(_ exactly: Components) -> Bool {
+ guard let newTC = __subtract(exactly: exactly, from: components) else { return false }
+
+ return setTimecode(exactly: newTC)
+ }
+
+ /// Subtract a duration from the current timecode.
+ /// Clamps to valid timecodes.
+ /// Input values can be as large as desired and will be calculated recursively. ie: (0,0,0,1000) or (0,0,500,0)
+ public mutating func subtract(clamping: Components) {
+ let newTC = __subtract(clamping: clamping, from: components)
+
+ _ = setTimecode(exactly: newTC)
+ }
+
+ /// Subtract a duration from the current timecode.
+ /// Wraps around the clock as set by the `upperLimit` property.
+ /// Input values can be as large as desired and will be calculated recursively. ie: (0,0,0,1000) or (0,0,500,0)
+ public mutating func subtract(wrapping: Components) {
+ let newTC = __subtract(wrapping: wrapping, from: components)
+
+ _ = setTimecode(exactly: newTC)
+ }
+
+ /// Subtract a duration from the current timecode and return a new instance with the new timecode.
+ /// Returns nil if resulting value is not within valid timecode range.
+ /// Input values can be as large as desired and will be calculated recursively. ie: (0,0,0,1000) or (0,0,500,0)
+ public func subtracting(_ exactly: Components) -> OTTimecode? {
+ guard let newTC = __subtract(exactly: exactly, from: components) else { return nil }
+
+ var newOTTimecode = self // copy self
+ guard newOTTimecode.setTimecode(exactly: newTC) else { return nil }
+
+ return newOTTimecode
+ }
+
+ /// Subtract a duration from the current timecode and return a new instance with the new timecode.
+ /// Clamps to valid timecodes.
+ /// Input values can be as large as desired and will be calculated recursively. ie: (0,0,0,1000) or (0,0,500,0)
+ public func subtracting(clamping values: Components) -> OTTimecode {
+ let newTC = __subtract(clamping: values, from: components)
+
+ var newOTTimecode = self // copy self
+ _ = newOTTimecode.setTimecode(exactly: newTC) // guaranteed to work
+
+ return newOTTimecode
+ }
+
+ /// Subtract a duration from the current timecode and return a new instance with the new timecode.
+ /// Wraps around the clock as set by the `upperLimit` property.
+ /// Input values can be as large as desired and will be calculated recursively. ie: (0,0,0,1000) or (0,0,500,0)
+ public func subtracting(wrapping values: Components) -> OTTimecode {
+ let newTC = __subtract(wrapping: values, from: components)
+
+ var newOTTimecode = self // copy self
+ _ = newOTTimecode.setTimecode(exactly: newTC) // guaranteed to work
+
+ return newOTTimecode
+ }
+
+
+ // MARK: - Multiply
+
+ /// Multiply the current timecode by an amount.
+ /// Returns false if resulting value is > the `upperLimit` property.
+ public mutating func multiply(_ exactly: Double) -> Bool {
+ guard let newTC = __multiply(exactly: exactly, with: components) else { return false }
+
+ return setTimecode(exactly: newTC)
+ }
+
+ /// Multiply the current timecode by an amount.
+ /// Clamps the result to the `upperLimit` property.
+ public mutating func multiply(clamping value: Double) {
+ let newTC = __multiply(clamping: value, with: components)
+
+ _ = setTimecode(exactly: newTC) // guaranteed to work
+ }
+
+ /// Multiply the current timecode by an amount.
+ /// Wraps around the clock as set by the `upperLimit` property.
+ public mutating func multiply(wrapping value: Double) {
+ let newTC = __multiply(wrapping: value, with: components)
+
+ _ = setTimecode(exactly: newTC) // guaranteed to work
+ }
+
+ /// Multiply a duration from the current timecode and return a new instance with the new timecode.
+ /// Returns nil if resulting value is not within valid timecode range.
+ /// Input values can be as large as desired and will be calculated recursively. ie: (0,0,0,1000) or (0,0,500,0)
+ public func multiplying(_ exactly: Double) -> OTTimecode? {
+ guard let newTC = __multiply(exactly: exactly, with: components) else { return nil }
+
+ var newOTTimecode = self // copy self
+ guard newOTTimecode.setTimecode(exactly: newTC) else { return nil }
+
+ return newOTTimecode
+ }
+
+ /// Multiply a duration from the current timecode and return a new instance with the new timecode.
+ /// Clamps to valid timecodes.
+ /// Input values can be as large as desired and will be calculated recursively. ie: (0,0,0,1000) or (0,0,500,0)
+ public func multiplying(clamping value: Double) -> OTTimecode {
+ let newTC = __multiply(clamping: value, with: components)
+
+ var newOTTimecode = self // copy self
+ _ = newOTTimecode.setTimecode(exactly: newTC) // guaranteed to work
+
+ return newOTTimecode
+ }
+
+ /// Multiply a duration from the current timecode and return a new instance with the new timecode.
+ /// Wraps around the clock as set by the `upperLimit` property.
+ /// Input values can be as large as desired and will be calculated recursively. ie: (0,0,0,1000) or (0,0,500,0)
+ public func multiplying(wrapping value: Double) -> OTTimecode {
+ let newTC = __multiply(wrapping: value, with: components)
+
+ var newOTTimecode = self // copy self
+ _ = newOTTimecode.setTimecode(exactly: newTC) // guaranteed to work
+
+ return newOTTimecode
+ }
+
+
+ // MARK: - Divide
+
+ /// Divide the current timecode by a duration.
+ /// Returns false if resulting value is > the `upperLimit` property.
+ public mutating func divide(_ exactly: Double) -> Bool {
+ guard let newTC = __divide(exactly: exactly, into: components) else { return false }
+
+ return setTimecode(exactly: newTC)
+ }
+
+ /// Divide the current timecode by a duration.
+ /// Clamps to valid timecodes.
+ public mutating func divide(clamping value: Double) {
+ let newTC = __divide(clamping: value, into: components)
+
+ _ = setTimecode(exactly: newTC) // guaranteed to work
+ }
+
+ /// Divide the current timecode by a duration.
+ /// Wraps around the clock as set by the `upperLimit` property.
+ public mutating func divide(wrapping value: Double) {
+ let newTC = __divide(wrapping: value, into: components)
+
+ _ = setTimecode(exactly: newTC) // guaranteed to work
+ }
+
+ /// Divide the current timecode by a duration and return a new instance with the new timecode.
+ /// Returns nil if resulting value is not within valid timecode range.
+ /// Input values can be as large as desired and will be calculated recursively. ie: (0,0,0,1000) or (0,0,500,0)
+ public func dividing(_ exactly: Double) -> OTTimecode? {
+ guard let newTC = __divide(exactly: exactly, into: components) else { return nil }
+
+ var newOTTimecode = self // copy self
+ guard newOTTimecode.setTimecode(exactly: newTC) else { return nil }
+
+ return newOTTimecode
+ }
+
+ /// Divide the current timecode by a duration and return a new instance with the new timecode.
+ /// Clamps to valid timecodes.
+ /// Input values can be as large as desired and will be calculated recursively. ie: (0,0,0,1000) or (0,0,500,0)
+ public func dividing(clamping value: Double) -> OTTimecode {
+ let newTC = __divide(clamping: value, into: components)
+
+ var newOTTimecode = self // copy self
+ _ = newOTTimecode.setTimecode(exactly: newTC) // guaranteed to work
+
+ return newOTTimecode
+ }
+
+ /// Divide the current timecode by a duration and return a new instance with the new timecode.
+ /// Wraps around the clock as set by the `upperLimit` property.
+ /// Input values can be as large as desired and will be calculated recursively. ie: (0,0,0,1000) or (0,0,500,0)
+ public func dividing(wrapping value: Double) -> OTTimecode {
+ let newTC = __divide(wrapping: value, into: components)
+
+ var newOTTimecode = self // copy self
+ _ = newOTTimecode.setTimecode(exactly: newTC) // guaranteed to work
+
+ return newOTTimecode
+ }
+
+}
diff --git a/Sources/SwiftTimecode/Math/OTTimecode Operators.swift b/Sources/SwiftTimecode/Math/OTTimecode Operators.swift
new file mode 100644
index 00000000..c327dc69
--- /dev/null
+++ b/Sources/SwiftTimecode/Math/OTTimecode Operators.swift
@@ -0,0 +1,54 @@
+//
+// OTTimecode Operators.swift
+// SwiftTimecode
+//
+// Created by Steffan Andrews on 2020-06-15.
+// Copyright © 2020 Steffan Andrews. All rights reserved.
+//
+
+import Foundation
+
+// MARK: - Math operators: Self, Self
+
+extension OTTimecode {
+
+ static public func +(lhs: Self, rhs: Self) -> OTTimecode {
+ return lhs.adding(clamping: rhs.components)
+ }
+
+ static public func +=(lhs: inout Self, rhs: Self) {
+ lhs.add(clamping: rhs.components)
+ }
+
+ static public func -(lhs: Self, rhs: Self) -> OTTimecode {
+ return lhs.subtracting(clamping: rhs.components)
+ }
+
+ static public func -=(lhs: inout Self, rhs: Self) {
+ lhs.subtract(clamping: rhs.components)
+ }
+
+}
+
+
+// MARK: - Math operators: Self, BinaryInteger
+
+extension OTTimecode {
+
+ static public func *(lhs: Self, rhs: T) -> Self {
+ return lhs.multiplying(clamping: Double(rhs))
+ }
+
+ static public func *=(lhs: inout Self, rhs: T) {
+ lhs.multiply(clamping: Double(rhs))
+ }
+
+ static public func /(lhs: Self, rhs: T) -> Self {
+ return lhs.dividing(clamping: Double(rhs))
+ }
+
+ static public func /=(lhs: inout Self, rhs: T) {
+ lhs.divide(clamping: Double(rhs))
+ }
+
+}
diff --git a/Sources/SwiftTimecode/OTTime/OTTime.swift b/Sources/SwiftTimecode/OTTime/OTTime.swift
new file mode 100644
index 00000000..ffc96d59
--- /dev/null
+++ b/Sources/SwiftTimecode/OTTime/OTTime.swift
@@ -0,0 +1,85 @@
+//
+// OTTime.swift
+// SwiftTimecode
+//
+// Created by Steffan Andrews on 2020-06-15.
+// Copyright © 2020 Steffan Andrews. All rights reserved.
+//
+
+import Foundation
+
+/// Primitive struct to represent real time.
+///
+/// In effort to retain precision, the value used when initializing will be stored unchanged, but can be accessed by either accessor. The backing value can be read as either `.seconds` or `.ms`. If the original format's property is get, the backing value will be returned unchanged. Otherwise it will be converted when needed when the properties are accessed.
+public struct OTTime {
+
+ // MARK: Data backing
+
+ private let msValue: Double?
+
+ private let secondsValue: Double?
+
+ // MARK: Public properties
+
+ public let backing: UnitBacking
+
+ public var ms: Double {
+ if msValue != nil { return msValue! }
+ if secondsValue != nil { return secondsValue! * 1000.0 }
+ return 0.0
+ }
+
+ public var seconds: Double {
+ if secondsValue != nil { return secondsValue! }
+ if msValue != nil { return msValue! / 1000.0 }
+ return 0.0
+ }
+
+ // MARK: init
+
+ public init(ms: Double) {
+ backing = .ms
+
+ msValue = ms
+ secondsValue = nil
+ }
+
+ public init(seconds: Double) {
+ backing = .seconds
+
+ msValue = nil
+ secondsValue = seconds
+ }
+
+}
+
+extension OTTime: Equatable {
+
+ public static func ==(lhs: Self, rhs: Self) -> Bool {
+ // limit precision to help ensure comparison is meaningful
+
+ return lhs.ms.truncated(decimalPlaces: 9) == rhs.ms.truncated(decimalPlaces: 9)
+ }
+
+}
+
+
+extension OTTime: Comparable {
+
+ public static func <(lhs: Self, rhs: Self) -> Bool {
+ // limit precision to help ensure comparison is meaningful
+
+ lhs.ms.truncated(decimalPlaces: 9) < rhs.ms.truncated(decimalPlaces: 9)
+ }
+
+}
+
+extension OTTime {
+
+ /// Enum describing units of time, as stored by `OTTime`
+ public enum UnitBacking {
+ case ms
+ case seconds
+ }
+
+}
diff --git a/Sources/SwiftTimecode/OTTimecode Elapsed Frames.swift b/Sources/SwiftTimecode/OTTimecode Elapsed Frames.swift
new file mode 100644
index 00000000..809dbbd5
--- /dev/null
+++ b/Sources/SwiftTimecode/OTTimecode Elapsed Frames.swift
@@ -0,0 +1,188 @@
+//
+// OTTimecode Elapsed Frames.swift
+// SwiftTimecode
+//
+// Created by Steffan Andrews on 2020-06-15.
+// Copyright © 2020 Steffan Andrews. All rights reserved.
+//
+
+import Foundation
+
+extension OTTimecode {
+
+ /// Returns the total number of whole frames elapsed from zero up to the timecode values.
+ /// When set, timecode is updated as long as the value passed is in valid range.
+ /// (Validation is based on the frame rate and `upperLimit` property.)
+ public var totalElapsedFrames: Double {
+
+ get {
+ return Self.totalElapsedFrames(of: components,
+ at: frameRate,
+ subFramesDivisor: subFramesDivisor)
+ }
+ set {
+ guard newValue >= 0.0
+ && newValue <= maxFramesAndSubframesExpressible
+ else { return }
+
+ let converted = Self.components(from: newValue,
+ at: frameRate,
+ subFramesDivisor: subFramesDivisor)
+
+ days = converted.d
+ hours = converted.h
+ minutes = converted.m
+ seconds = converted.s
+ frames = converted.f
+ subFrames = converted.sf
+ }
+
+ }
+
+}
+
+
+// MARK: - Static methods
+
+extension OTTimecode {
+
+ /// Internal func: calculates total frames from given values at the current frame rate.
+ /// (To keep values clean, instead of returning a Double including subframes, this returns an Int with total frames, truncating the subframes. You can then add subframes to this return value to get a floating-point number representing total frames to the subframe precision in a single value.)
+ public static func totalElapsedFrames(of values: Components,
+ at frameRate: FrameRate,
+ subFramesDivisor: Int? = nil) -> Double
+ {
+
+ let subFramesUnitInterval =
+ subFramesDivisor == nil
+ ? 0.0
+ : Double(values.sf) / Double(subFramesDivisor!)
+
+ switch frameRate.isDrop {
+ case true:
+ let totalMinutes = (24 * 60 * values.d) + (60 * values.h) + values.m
+
+ let totalWholeFrames =
+ (
+ (frameRate.maxFrames * 60 * 60 * 24 * values.d)
+ + (frameRate.maxFrames * 60 * 60 * values.h)
+ + (frameRate.maxFrames * 60 * values.m)
+ + (frameRate.maxFrames * values.s)
+ + (values.f)
+ )
+
+ - (Int(frameRate.framesDroppedPerMinute) * (totalMinutes - (totalMinutes / 10)))
+
+ let totalFramesPlusSubframes = Double(totalWholeFrames) + subFramesUnitInterval
+
+ return totalFramesPlusSubframes
+
+ case false:
+ let dd = Double(values.d) * 24 * 60 * 60 * frameRate.frameRateForElapsedFramesCalculation
+ let hh = Double(values.h) * 60 * 60 * frameRate.frameRateForElapsedFramesCalculation
+ let mm = Double(values.m) * 60 * frameRate.frameRateForElapsedFramesCalculation
+ let ss = Double(values.s) * frameRate.frameRateForElapsedFramesCalculation
+ let totalWholeFrames = Int(round(dd + hh + mm + ss)) + values.f
+
+ let totalFramesPlusSubframes = Double(totalWholeFrames) + subFramesUnitInterval
+
+ return totalFramesPlusSubframes
+
+ }
+
+ }
+
+ /// Internal func: calculates resulting values from total frames at the current frame rate.
+ /// (You can add subframes afterward to the `sf` property if needed.)
+ public static func components(from totalElapsedFrames: Int,
+ at frameRate: FrameRate,
+ subFramesDivisor: Int?) -> Components
+ {
+
+ return components(from: Double(totalElapsedFrames),
+ at: frameRate,
+ subFramesDivisor: subFramesDivisor)
+
+ }
+
+ /// Internal func: calculates resulting values from total frames at the current frame rate.
+ /// (You can add subframes afterward to the `sf` property if needed.)
+ public static func components(from totalElapsedFrames: Double,
+ at frameRate: FrameRate,
+ subFramesDivisor: Int?) -> Components
+ {
+
+ // prep vars
+
+ var dd = 00
+ var hh = 00
+ var mm = 00
+ var ss = 00
+ var ff = 00
+ var sf = 00
+
+ var inElapsedFrames = Double(totalElapsedFrames)
+
+ // drop frame
+
+ if frameRate.isDrop {
+
+ // modify input elapsed frame count in the case of a drop-frame frame rate so it can be converted
+
+ let framesPer10Minutes = frameRate.frameRateForElapsedFramesCalculation * 600
+
+ let D = floor(inElapsedFrames / framesPer10Minutes)
+
+ let M = inElapsedFrames.truncatingRemainder(dividingBy: framesPer10Minutes)
+
+ let F = max(0, M - frameRate.framesDroppedPerMinute) // don't allow negative numbers
+
+ inElapsedFrames =
+ inElapsedFrames
+ + (9 * frameRate.framesDroppedPerMinute * D)
+ + (frameRate.framesDroppedPerMinute
+ * floor(F / ((framesPer10Minutes - frameRate.framesDroppedPerMinute) / 10)) )
+
+ }
+
+ // final calculation
+
+ let frMaxFrames = Double(frameRate.maxFrames)
+
+ dd = Int(
+ (inElapsedFrames / (frMaxFrames * 60 * 60 * 24))
+ .floor
+ )
+
+ hh = Int(
+ (inElapsedFrames / (frMaxFrames * 60 * 60))
+ .floor
+ .truncatingRemainder(dividingBy: 24)
+ )
+
+ mm = Int(
+ (inElapsedFrames / (frMaxFrames * 60))
+ .floor
+ .truncatingRemainder(dividingBy: 60)
+ )
+
+ ss = Int(
+ (inElapsedFrames / frMaxFrames)
+ .floor
+ .truncatingRemainder(dividingBy: 60)
+ )
+
+ ff = Int(
+ inElapsedFrames
+ .truncatingRemainder(dividingBy: frMaxFrames)
+ )
+
+ if let sfDiv = subFramesDivisor {
+ sf = Int(inElapsedFrames.fraction * Double(sfDiv))
+ }
+
+ return Components(d: dd, h: hh, m: mm, s: ss, f: ff, sf: sf)
+
+ }
+
+}
diff --git a/Sources/SwiftTimecode/OTTimecode String Extensions.swift b/Sources/SwiftTimecode/OTTimecode String Extensions.swift
new file mode 100644
index 00000000..f10ad6fa
--- /dev/null
+++ b/Sources/SwiftTimecode/OTTimecode String Extensions.swift
@@ -0,0 +1,63 @@
+//
+// OTTimecode String Extensions.swift
+// SwiftTimecode
+//
+// Created by Steffan Andrews on 2020-06-15.
+// Copyright © 2020 Steffan Andrews. All rights reserved.
+//
+
+import Foundation
+
+// MARK: - to OTTimecode
+
+extension String {
+
+ /// Returns an instance of `OTTimecode(exactly:)`.
+ /// If the string is not a valid timecode string, it returns nil.
+ public func toTimecode(at frameRate: OTTimecode.FrameRate,
+ limit: OTTimecode.UpperLimit = ._24hours,
+ subFramesDivisor: Int? = nil) -> OTTimecode?
+ {
+
+ if let sfd = subFramesDivisor {
+
+ return OTTimecode(self,
+ at: frameRate,
+ limit: limit,
+ subFramesDivisor: sfd)
+
+ } else {
+
+ return OTTimecode(self,
+ at: frameRate,
+ limit: limit)
+
+ }
+
+ }
+
+ /// Returns an instance of `OTTimecode(rawValues:)`.
+ /// If the string is not a valid timecode string, it returns nil.
+ public func toTimecode(rawValuesAt frameRate: OTTimecode.FrameRate,
+ limit: OTTimecode.UpperLimit = ._24hours,
+ subFramesDivisor: Int? = nil) -> OTTimecode?
+ {
+
+ if let sfd = subFramesDivisor {
+
+ return OTTimecode(rawValues: self,
+ at: frameRate,
+ limit: limit,
+ subFramesDivisor: sfd)
+
+ } else {
+
+ return OTTimecode(rawValues: self,
+ at: frameRate,
+ limit: limit)
+
+ }
+
+ }
+
+}
diff --git a/Sources/SwiftTimecode/OTTimecode Validation.swift b/Sources/SwiftTimecode/OTTimecode Validation.swift
new file mode 100644
index 00000000..469443ee
--- /dev/null
+++ b/Sources/SwiftTimecode/OTTimecode Validation.swift
@@ -0,0 +1,235 @@
+//
+// OTTimecode Validation.swift
+// SwiftTimecode
+//
+// Created by Steffan Andrews on 2020-06-15.
+// Copyright © 2020 Steffan Andrews. All rights reserved.
+//
+
+import Foundation
+
+extension OTTimecode {
+
+ /// Returns a set of invalid components, if any.
+ /// A fully valid timecode will return an empty set.
+ /// Validation relies on `frameRate` and `upperLimit`.
+ public var invalidComponents: Set
+ {
+
+ Self.invalidComponents(in: self.components,
+ at: frameRate,
+ limit: upperLimit,
+ subFramesDivisor: subFramesDivisor)
+
+ }
+
+}
+
+extension OTTimecode.Components {
+
+ /// Returns a set of invalid components, if any.
+ /// A fully valid timecode will return an empty set.
+ public func invalidComponents(at frameRate: OTTimecode.FrameRate,
+ limit: OTTimecode.UpperLimit,
+ subFramesDivisor: Int) -> Set
+ {
+
+ OTTimecode.invalidComponents(in: self,
+ at: frameRate,
+ limit: limit,
+ subFramesDivisor: subFramesDivisor)
+
+ }
+
+}
+
+extension OTTimecode {
+
+ /// Returns a set of invalid components, if any.
+ /// A fully valid timecode will return an empty set.
+ public static func invalidComponents(in components: TCC,
+ at frameRate: FrameRate,
+ limit: UpperLimit,
+ subFramesDivisor: Int) -> Set
+ {
+
+ var invalids: Set = []
+
+ // days
+
+ if !components.validRange(of: .days,
+ at: frameRate,
+ limit: limit,
+ subFramesDivisor: subFramesDivisor)
+ .contains(components.d)
+ { invalids.insert(.days) }
+
+ // hours
+
+ if !components.validRange(of: .hours,
+ at: frameRate,
+ limit: limit,
+ subFramesDivisor: subFramesDivisor)
+ .contains(components.h)
+ { invalids.insert(.hours) }
+
+ // minutes
+
+ if !components.validRange(of: .minutes,
+ at: frameRate,
+ limit: limit,
+ subFramesDivisor: subFramesDivisor)
+ .contains(components.m)
+ { invalids.insert(.minutes) }
+
+ // seconds
+
+ if !components.validRange(of: .seconds,
+ at: frameRate,
+ limit: limit,
+ subFramesDivisor: subFramesDivisor)
+ .contains(components.s)
+ { invalids.insert(.seconds) }
+
+ // frames
+ if !components.validRange(of: .frames,
+ at: frameRate,
+ limit: limit,
+ subFramesDivisor: subFramesDivisor)
+ .contains(components.f)
+ { invalids.insert(.frames) }
+
+ // subframes
+ if !components.validRange(of: .subFrames,
+ at: frameRate,
+ limit: limit,
+ subFramesDivisor: subFramesDivisor)
+ .contains(components.sf)
+ { invalids.insert(.subFrames) }
+
+ return invalids
+
+ }
+
+}
+
+extension OTTimecode {
+
+ /// Returns valid range of values for a timecdoe component, given the current `frameRate` and `upperLimit`.
+ public func validRange(of component: Component) -> (ClosedRange)
+ {
+
+ components.validRange(of: component,
+ at: frameRate,
+ limit: upperLimit,
+ subFramesDivisor: subFramesDivisor)
+
+ }
+
+}
+
+extension OTTimecode.Components {
+
+ /// Returns valid range of values for a timecdoe component.
+ public func validRange(of component: OTTimecode.Component,
+ at frameRate: OTTimecode.FrameRate,
+ limit: OTTimecode.UpperLimit,
+ subFramesDivisor: Int) -> (ClosedRange)
+ {
+
+ switch component {
+
+ case .days:
+ return 0...limit.maxDaysExpressible
+
+ case .hours:
+ return 0...23
+
+ case .minutes:
+ return 0...59
+
+ case .seconds:
+ return 0...59
+
+ case .frames:
+ let startFramePossible = frameRate.isDrop
+ ? ((m % 10 != 0 && s == 0) ? 2 : 0)
+ : 0
+
+ return startFramePossible...frameRate.maxFrameNumberDisplayable
+
+ case .subFrames:
+ // clamp divisor to prevent a possible crash if subFramesDivisor < 0
+ return 0...(subFramesDivisor.clamped(to: 1...) - 1)
+
+ }
+
+ }
+
+}
+
+extension OTTimecode {
+
+ internal mutating func __clamp(component: Component) {
+
+ switch component {
+ case .days:
+ days = days.clamped(to: validRange(of: .days))
+
+ case .hours:
+ hours = hours.clamped(to: validRange(of: .hours))
+
+ case .minutes:
+ minutes = minutes.clamped(to: validRange(of: .minutes))
+
+ case .seconds:
+ seconds = seconds.clamped(to: validRange(of: .seconds))
+
+ case .frames:
+ frames = frames.clamped(to: validRange(of: .frames))
+
+ case .subFrames:
+ subFrames = subFrames.clamped(to: validRange(of: .subFrames))
+
+ }
+
+ }
+
+}
+
+extension OTTimecode {
+
+ /// Validates and clamps all timecode components to valid values at the current `frameRate` and `upperLimit` bound.
+ public mutating func clampComponents() {
+
+ __clamp(component: .days)
+ __clamp(component: .hours)
+ __clamp(component: .minutes)
+ __clamp(component: .seconds)
+ __clamp(component: .frames)
+ __clamp(component: .subFrames)
+
+ }
+
+}
+
+extension OTTimecode {
+
+ /// Returns the largest subframe value displayable before rolling over to the next frame.
+ public var maxSubFramesExpressible: Int {
+
+ validRange(of: .subFrames)
+ .upperBound
+
+ }
+
+ /// Returns the `upperLimit` minus 1 subframe.
+ public var maxFramesAndSubframesExpressible: Double {
+
+ Double(frameRate.maxTotalFramesExpressible(in: upperLimit))
+ + Double(maxSubFramesExpressible)
+ / Double(subFramesDivisor)
+
+ }
+
+}
diff --git a/Sources/SwiftTimecode/OTTimecode init.swift b/Sources/SwiftTimecode/OTTimecode init.swift
new file mode 100644
index 00000000..4496cf68
--- /dev/null
+++ b/Sources/SwiftTimecode/OTTimecode init.swift
@@ -0,0 +1,208 @@
+//
+// OTTimecode init.swift
+// SwiftTimecode
+//
+// Created by Steffan Andrews on 2020-06-15.
+// Copyright © 2020 Steffan Andrews. All rights reserved.
+//
+
+import Foundation
+
+extension OTTimecode {
+
+ // MARK: Basic
+
+ /// Instance with default values.
+ public init(at frameRate: FrameRate,
+ limit: UpperLimit = ._24hours,
+ subFramesDivisor: Int = 80)
+ {
+
+ self.frameRate = frameRate
+ self.upperLimit = limit
+ self.subFramesDivisor = subFramesDivisor
+
+ }
+
+
+ // MARK: Components
+
+ /** Instance with timecode values and frame rate. Optionally set `upperLimit`.
+ Values which are out-of-bounds will return nil.
+ (Validation is based on the frame rate and `upperLimit` property.)
+ */
+ public init?(_ exactly: Components,
+ at frameRate: FrameRate,
+ limit: UpperLimit = ._24hours,
+ subFramesDivisor: Int = 80)
+ {
+
+ self.frameRate = frameRate
+ self.upperLimit = limit
+ self.subFramesDivisor = subFramesDivisor
+
+ if !setTimecode(exactly: exactly) { return nil }
+
+ }
+
+ /** Instance with timecode values and frame rate. Optionally set `upperLimit`.
+ Values which are out-of-bounds will be clamped to minimum or maximum possible values.
+ (Clamping is based on the frame rate and `upperLimit` property.)
+ */
+ public init(clamping: Components,
+ at frameRate: FrameRate,
+ limit: UpperLimit = ._24hours,
+ subFramesDivisor: Int = 80)
+ {
+
+ self.frameRate = frameRate
+ self.upperLimit = limit
+ self.subFramesDivisor = subFramesDivisor
+
+ setTimecode(clamping: clamping)
+
+ }
+
+ /** Instance with timecode values and frame rate. Optionally set `upperLimit`.
+ Timecodes will be wrapped around the timecode clock if out-of-bounds.
+ (Wrapping is based on the frame rate and `upperLimit` property.)
+ */
+ public init(wrapping: Components,
+ at frameRate: FrameRate,
+ limit: UpperLimit = ._24hours,
+ subFramesDivisor: Int = 80)
+ {
+
+ self.frameRate = frameRate
+ self.upperLimit = limit
+ self.subFramesDivisor = subFramesDivisor
+
+ setTimecode(wrapping: wrapping)
+
+ }
+
+ /** Instance with timecode values and frame rate.
+ Timecode values will not be validated or rejected if they overflow.
+ */
+ public init(rawValues: Components,
+ at frameRate: FrameRate,
+ limit: UpperLimit = ._24hours,
+ subFramesDivisor: Int = 80)
+ {
+
+ self.frameRate = frameRate
+ self.upperLimit = limit
+ self.subFramesDivisor = subFramesDivisor
+
+ setTimecode(rawValues: rawValues)
+
+ }
+
+
+ // MARK: String
+
+ /** Instance with timecode string and frame rate. Optionally set `upperLimit`.
+ An improperly formatted timecode string or one with invalid values will return nil.
+ (Validation is based on the frame rate and `upperLimit` property.)
+ */
+ public init?(_ exactly: String,
+ at frameRate: FrameRate,
+ limit: UpperLimit = ._24hours,
+ subFramesDivisor: Int = 80)
+ {
+
+ self.frameRate = frameRate
+ self.upperLimit = limit
+ self.subFramesDivisor = subFramesDivisor
+
+ if !setTimecode(exactly: exactly) { return nil }
+
+ }
+
+ /** Instance with timecode string and frame rate. Optionally set `upperLimit`.
+ Values which are out-of-bounds will be clamped to minimum or maximum possible values.
+ (Clamping is based on the frame rate and `upperLimit` property.)
+ */
+ public init?(clamping: String,
+ at frameRate: FrameRate,
+ limit: UpperLimit = ._24hours,
+ subFramesDivisor: Int = 80)
+ {
+
+ self.frameRate = frameRate
+ self.upperLimit = limit
+ self.subFramesDivisor = subFramesDivisor
+
+ if !setTimecode(clamping: clamping) { return nil } // validation is triggered by this method
+
+ }
+
+ /** Instance with timecode string and frame rate. Optionally set `upperLimit`.
+ An improperly formatted timecode string or one with invalid values will return nil.
+ (Wrapping is based on the frame rate and `upperLimit` property.)
+ */
+ public init?(wrapping: String,
+ at frameRate: FrameRate,
+ limit: UpperLimit = ._24hours,
+ subFramesDivisor: Int = 80)
+ {
+
+ self.frameRate = frameRate
+ self.upperLimit = limit
+ self.subFramesDivisor = subFramesDivisor
+
+ if !setTimecode(wrapping: wrapping) { return nil }
+
+ }
+
+ /** Instance with timecode values and frame rate.
+ Timecode values will not be validated or rejected if they overflow.
+ */
+ public init?(rawValues: String,
+ at frameRate: FrameRate,
+ limit: UpperLimit = ._24hours,
+ subFramesDivisor: Int = 80)
+ {
+
+ self.frameRate = frameRate
+ self.upperLimit = limit
+ self.subFramesDivisor = subFramesDivisor
+
+ if !setTimecode(rawValues: rawValues) { return nil }
+
+ }
+
+ // MARK: Real time
+
+ /// Instance with real time and frame rate. Optionally set `upperLimit`.
+ public init?(_ lossy: OTTime,
+ at frameRate: FrameRate,
+ limit: UpperLimit = ._24hours,
+ subFramesDivisor: Int = 80)
+ {
+
+ self.frameRate = frameRate
+ self.upperLimit = limit
+ self.subFramesDivisor = subFramesDivisor
+
+ if !self.setTimecode(from: lossy) { return nil }
+
+ }
+
+ /// Instance with real time and frame rate. Optionally set `upperLimit`.
+ public init?(samples: Double,
+ sampleRate: Int,
+ at frameRate: FrameRate,
+ limit: UpperLimit = ._24hours,
+ subFramesDivisor: Int = 80)
+ {
+
+ self.frameRate = frameRate
+ self.upperLimit = limit
+ self.subFramesDivisor = subFramesDivisor
+
+ if !self.setTimecode(fromSamplesValue: samples, atSampleRate: sampleRate) { return nil }
+
+ }
+
+}
diff --git a/Sources/SwiftTimecode/OTTimecode.swift b/Sources/SwiftTimecode/OTTimecode.swift
new file mode 100644
index 00000000..9d1bb470
--- /dev/null
+++ b/Sources/SwiftTimecode/OTTimecode.swift
@@ -0,0 +1,90 @@
+//
+// OTTimecode.swift
+// SwiftTimecode
+//
+// Created by Steffan Andrews on 2018-07-08.
+// Copyright © 2018 Steffan Andrews. All rights reserved.
+//
+
+import Foundation
+
+
+/// Object representing SMPTE timecode data with a variety of number- and string- based constructors, including helper functions to convert between them operators to perform math operations between them.
+public struct OTTimecode {
+
+ // MARK: - Immutable properties
+
+ /// Frame rate.
+ ///
+ /// Note: Several properties are available on the frame rate that is selected, including its `.stringValue` representation or whether the rate `.isDrop`.
+ ///
+ /// Setting this value directly does not trigger any validation.
+ public let frameRate: FrameRate
+
+ /// Timecode maximum upper bound.
+ ///
+ /// This also affects how timecode values wrap when adding or clamping.
+ ///
+ /// Setting this value directly does not trigger any validation.
+ public let upperLimit: UpperLimit
+
+ /// Subframes divisor.
+ ///
+ /// The number of subframes that make up a single frame.
+ ///
+ /// (ie: a divisor of 80 implies a range of 0...79)
+ ///
+ /// This will vary depending on application. Most common divisors are 80 or 100.
+ public let subFramesDivisor: Int
+
+ /// Determines whether subframes are included when getting `.stringValue`.
+ ///
+ /// This does not disable subframes from being stored or calculated, only whether they are output in the string.
+ public var displaySubFrames: Bool = false
+
+ // MARK: - Mutable properties
+
+ /// Timecode days.
+ ///
+ /// Valid only if `.upperLimit` is set to `._100days`.
+ ///
+ /// Setting this value directly does not trigger any validation.
+ public var days: Int = 0
+
+ /// Timecode hours.
+ ///
+ /// Valid range: 0-23.
+ ///
+ /// Setting this value directly does not trigger any validation.
+ public var hours: Int = 0
+
+ /// Timecode minutes.
+ ///
+ /// Valid range: 0-59.
+ ///
+ /// Setting this value directly does not trigger any validation.
+ public var minutes: Int = 0
+
+ /// Timecode seconds.
+ ///
+ /// Valid range: 0-59.
+ ///
+ /// Setting this value directly does not trigger any validation.
+ public var seconds: Int = 0
+
+ /// Timecode frames.
+ ///
+ /// Valid range is dependent on the `frameRate` property (0-23 for 24NDF, 0-29 for 30NDF, 2-29 every minute except 0-29 for every 10th minute for 29.97DF, etc.).
+ ///
+ /// Setting this value directly does not trigger any validation.
+ public var frames: Int = 0
+
+ /// Timecode subframe component.
+ ///
+ /// To remain platform agnostic, this value is represented as a floating-point unit interval (0.0...1.0) since there are no consistent standards on subframe divisions.
+ /// (ie: traditionally Cubase/Nuendo and Logic Pro use 80 subframes, Pro Tools uses 100 subframes, etc.)
+ ///
+ /// Setting this value directly does not trigger any validation.
+ public var subFrames: Int = 0
+
+}
diff --git a/Sources/SwiftTimecode/Protocol Adoptions/Comparable.swift b/Sources/SwiftTimecode/Protocol Adoptions/Comparable.swift
new file mode 100644
index 00000000..d1bd2150
--- /dev/null
+++ b/Sources/SwiftTimecode/Protocol Adoptions/Comparable.swift
@@ -0,0 +1,25 @@
+//
+// Comparable.swift
+// SwiftTimecode
+//
+// Created by Steffan Andrews on 2020-06-15.
+// Copyright © 2020 Steffan Andrews. All rights reserved.
+//
+
+import Foundation
+
+extension OTTimecode: Equatable {
+
+ static public func ==(lhs: Self, rhs: Self) -> Bool {
+ return lhs.realTime == rhs.realTime
+ }
+
+}
+
+extension OTTimecode: Comparable {
+
+ static public func <(lhs: Self, rhs: Self) -> Bool {
+ return (lhs.realTime < rhs.realTime)
+ }
+
+}
diff --git a/Sources/SwiftTimecode/Protocol Adoptions/CustomStringConvertible.swift b/Sources/SwiftTimecode/Protocol Adoptions/CustomStringConvertible.swift
new file mode 100644
index 00000000..aada647b
--- /dev/null
+++ b/Sources/SwiftTimecode/Protocol Adoptions/CustomStringConvertible.swift
@@ -0,0 +1,35 @@
+//
+// CustomStringConvertible.swift
+// SwiftTimecode
+//
+// Created by Steffan Andrews on 2020-08-17.
+// Copyright © 2020 Steffan Andrews. All rights reserved.
+//
+
+import Foundation
+
+extension OTTimecode: CustomStringConvertible, CustomDebugStringConvertible {
+
+ public var description: String {
+
+ return stringValue
+
+ }
+
+ public var debugDescription: String {
+ // include Days even if it's 0 if we have a mode set that enables Days
+ let daysString =
+ upperLimit == UpperLimit._100days
+ ? "\(days):"
+ : ""
+
+ return "OTTimecode<\(daysString)\(stringValue) @ \(frameRate.stringValue)>"
+ }
+
+ public var verboseDescription: String {
+
+ return "\(stringValue) @ \(frameRate.stringValue)"
+
+ }
+
+}
diff --git a/Sources/SwiftTimecode/Protocol Adoptions/Hashable.swift b/Sources/SwiftTimecode/Protocol Adoptions/Hashable.swift
new file mode 100644
index 00000000..efa7e659
--- /dev/null
+++ b/Sources/SwiftTimecode/Protocol Adoptions/Hashable.swift
@@ -0,0 +1,27 @@
+//
+// Hashable.swift
+// SwiftTimecode
+//
+// Created by Steffan Andrews on 2020-06-15.
+// Copyright © 2020 Steffan Andrews. All rights reserved.
+//
+
+import Foundation
+
+extension OTTimecode: Hashable {
+
+ // This relies on "enum FrameRate: Int"
+
+ // To make the struct truly hashed, any crucial differentiating attributes must be encoded...
+ // Max frames of 100 days @ 120fps ==
+ // 0b11 1101 1100 1100 0101 0000 0000 0000 -- 30 bits
+
+ public func hash(into hasher: inout Hasher) {
+ // Add the framerate information in bits above the total frames value; 30 places to the left so they don't overlap
+
+ hasher.combine(totalElapsedFrames)
+ hasher.combine(frameRate.rawValue)
+
+ }
+
+}
diff --git a/Sources/SwiftTimecode/Protocol Adoptions/Strideable.swift b/Sources/SwiftTimecode/Protocol Adoptions/Strideable.swift
new file mode 100644
index 00000000..f8c30546
--- /dev/null
+++ b/Sources/SwiftTimecode/Protocol Adoptions/Strideable.swift
@@ -0,0 +1,32 @@
+//
+// Strideable.swift
+// SwiftTimecode
+//
+// Created by Steffan Andrews on 2018-07-20.
+// Copyright © 2018 Steffan Andrews. All rights reserved.
+//
+
+import Foundation
+
+extension OTTimecode: Strideable {
+
+ public typealias Stride = Int
+
+ /// Returns a new instance advanced by specified time components.
+ /// Same as calling `.adding(clamping: TCC(f: n))` but implemented in order to allow OTTimecode to conform to `Strideable`.
+ /// Will clamp to valid timecode range.
+ public func advanced(by n: Int) -> Self {
+
+ return self.adding(clamping: Components(f: n))
+
+ }
+
+ /// Distance between two timecodes expressed as number of frames.
+ /// Implemented in order to allow OTTimecode to conform to `Strideable`.
+ public func distance(to other: Self) -> Int {
+
+ return Int(trunc(other.totalElapsedFrames) - trunc(self.totalElapsedFrames))
+
+ }
+
+}
diff --git a/Sources/SwiftTimecode/UI/TextField.swift b/Sources/SwiftTimecode/UI/TextField.swift
new file mode 100644
index 00000000..8992d15f
--- /dev/null
+++ b/Sources/SwiftTimecode/UI/TextField.swift
@@ -0,0 +1,65 @@
+//
+// TextField.swift
+// SwiftTimecode
+//
+// Created by Steffan Andrews on 2020-07-11.
+// Copyright © 2020 Steffan Andrews. All rights reserved.
+//
+
+#if os(macOS)
+
+import Cocoa
+
+extension OTTimecode {
+
+ /// NSTextField subclass with timecode formatting
+ ///
+ /// Formatter is in effect bypassed until all its properties are set (frameRate, upperLimit, displaySubFrames, subFramesDivisor). These can be set after the class has initialized.
+ ///
+ /// See `.formatter` property to access these.
+ @objc(OTTimecodeTextField)
+ public class TextField: NSTextField {
+
+ public required init?(coder: NSCoder) {
+ super.init(coder: coder)
+
+ self.formatter = TextFormatter()
+
+ self.allowsEditingTextAttributes = false
+ self.cell?.allowsEditingTextAttributes = false
+ }
+
+ // responder chain: triggered when user presses Esc key
+ public override func cancelOperation(_ sender: Any?) {
+ //super.cancelOperation(sender)
+
+ // cancel changes to text
+ abortEditing()
+
+ // give focus back to self
+ window?.makeFirstResponder(self)
+ }
+
+ }
+
+}
+
+extension OTTimecode {
+
+ /// NSTextFieldCell subclass with timecode formatting
+ @objc(OTTimecodeTextFieldCell)
+ public class TextFieldCell: NSTextFieldCell {
+
+ public required init(coder: NSCoder) {
+ super.init(coder: coder)
+
+ formatter = TextFormatter()
+
+ self.allowsEditingTextAttributes = false
+ }
+
+ }
+
+}
+
+#endif
diff --git a/Sources/SwiftTimecode/UpperLimit/UpperLimit.swift b/Sources/SwiftTimecode/UpperLimit/UpperLimit.swift
new file mode 100644
index 00000000..0a76564a
--- /dev/null
+++ b/Sources/SwiftTimecode/UpperLimit/UpperLimit.swift
@@ -0,0 +1,64 @@
+//
+// Extent.swift
+// SwiftTimecode
+//
+// Created by Steffan Andrews on 2020-06-15.
+// Copyright © 2020 Steffan Andrews. All rights reserved.
+//
+
+import Foundation
+
+extension OTTimecode {
+
+ /// Enum describing the maximum timecode ceiling.
+ public enum UpperLimit: Int {
+
+ /// Pro Tools' upper limit is "23:59:59:FF" which is 1 day (24 hours) in duration.
+ case _24hours
+
+ /// Cubase's upper limit is "99 23:59:59:FF" which is 100 days in duration.
+ case _100days
+
+ /// Internal use.
+ internal var maxDays: Int {
+ switch self {
+ case ._24hours: return 1
+ case ._100days: return 100
+ }
+ }
+
+ /// Internal use.
+ internal var maxDaysExpressible: Int {
+ switch self {
+ case ._24hours: return maxDays - 1
+ case ._100days: return maxDays - 1
+ }
+ }
+
+ /// Internal use.
+ internal var maxHours: Int {
+ switch self {
+ case ._24hours: return 24
+ case ._100days: return 24
+ }
+ }
+
+ /// Internal use.
+ internal var maxHoursExpressible: Int {
+ switch self {
+ case ._24hours: return maxHours - 1
+ case ._100days: return maxHours - 1
+ }
+ }
+
+ /// Internal use.
+ internal var maxHoursTotal: Int {
+ switch self {
+ case ._24hours: return maxDays - 1
+ case ._100days: return (24 * maxDays) - 1
+ }
+ }
+
+ }
+
+}
diff --git a/Sources/SwiftTimecode/Utilities/BinaryInteger.swift b/Sources/SwiftTimecode/Utilities/BinaryInteger.swift
new file mode 100644
index 00000000..62cae31b
--- /dev/null
+++ b/Sources/SwiftTimecode/Utilities/BinaryInteger.swift
@@ -0,0 +1,30 @@
+//
+// File.swift
+// SwiftTimecode
+//
+// Created by Steffan Andrews on 2020-09-17.
+//
+
+import Foundation
+
+extension BinaryInteger {
+
+ /// CUSTOM SHARED:
+ /// Returns number of digits (places to the left of the decimal) in the number.
+ ///
+ /// ie:
+ /// - for the integer 0, this would return 1
+ /// - for the integer 5, this would return 1
+ /// - for the integer 10, this would return 2
+ /// - for the integer 250, this would return 3
+ internal var numberOfDigits: Int {
+
+ if self < 10 && self >= 0 || self > -10 && self < 0 {
+ return 1
+ } else {
+ return 1 + (self / 10).numberOfDigits
+ }
+
+ }
+
+}
diff --git a/Sources/SwiftTimecode/Utilities/CharacterSet.swift b/Sources/SwiftTimecode/Utilities/CharacterSet.swift
new file mode 100644
index 00000000..4c50c58e
--- /dev/null
+++ b/Sources/SwiftTimecode/Utilities/CharacterSet.swift
@@ -0,0 +1,22 @@
+//
+// File.swift
+// SwiftTimecode
+//
+// Created by Steffan Andrews on 2020-09-17.
+//
+
+import Foundation
+
+extension CharacterSet {
+
+ /// CUSTOM SHARED:
+ /// Returns true if the CharacterSet contains the given Character.
+ internal func contains(_ character: Character) -> Bool {
+
+ return character
+ .unicodeScalars
+ .allSatisfy(contains(_:))
+
+ }
+
+}
diff --git a/Sources/SwiftTimecode/Utilities/Clamped.swift b/Sources/SwiftTimecode/Utilities/Clamped.swift
new file mode 100644
index 00000000..d7667d43
--- /dev/null
+++ b/Sources/SwiftTimecode/Utilities/Clamped.swift
@@ -0,0 +1,70 @@
+//
+// File.swift
+// SwiftTimecode
+//
+// Created by Steffan Andrews on 2020-09-17.
+//
+
+import Foundation
+
+// MARK: - .clamped(to:)
+
+extension Comparable {
+
+ // ie: 5.clamped(to: 7...10)
+ // ie: 5.0.clamped(to: 7.0...10.0)
+ // ie: "a".clamped(to: "b"..."h")
+ /// CUSTOM SHARED:
+ /// Returns the value clamped to the passed range.
+ internal func clamped(to limits: ClosedRange) -> Self {
+ return min(max(self, limits.lowerBound), limits.upperBound)
+ }
+
+ // ie: 5.clamped(to: 300...)
+ // ie: 5.0.clamped(to: 300.00...)
+ // ie: "a".clamped(to: "b"...)
+ /// CUSTOM SHARED:
+ /// Returns the value clamped to the passed range.
+ internal func clamped(to limits: PartialRangeFrom) -> Self {
+ return max(self, limits.lowerBound)
+ }
+
+ // ie: 400.clamped(to: ...300)
+ // ie: 400.0.clamped(to: ...300.0)
+ // ie: "k".clamped(to: ..."h")
+ /// CUSTOM SHARED:
+ /// Returns the value clamped to the passed range.
+ internal func clamped(to limits: PartialRangeThrough) -> Self {
+ return min(self, limits.upperBound)
+ }
+
+ // ie: 5.0.clamped(to: 7.0..<10.0)
+ // not a good idea to implement this -- floating point numbers don't make sense in a ..< type range
+ // because would the max of 7.0..<10.0 be 9.999999999...? It can't be 10.0.
+ // func clamped(to limits: Range) -> Self { }
+
+}
+
+extension Strideable {
+
+ // ie: 400.clamped(to: ..<300)
+ // won't work for String
+ /// CUSTOM SHARED:
+ /// Returns the value clamped to the passed range.
+ internal func clamped(to limits: PartialRangeUpTo) -> Self {
+ return min(self, limits.upperBound.advanced(by: -1)) // advanced(by:) requires Strideable, not available on just Comparable
+ }
+
+}
+
+extension Strideable where Self.Stride: SignedInteger {
+
+ // ie: 5.clamped(to: 7..<10)
+ // won't work for String
+ /// CUSTOM SHARED:
+ /// Returns the value clamped to the passed range.
+ internal func clamped(to limits: Range) -> Self {
+ return min(max(self, limits.lowerBound), limits.index(before: limits.upperBound)) // index(before:) only available on SignedInteger
+ }
+
+}
diff --git a/Sources/SwiftTimecode/Utilities/File.swift b/Sources/SwiftTimecode/Utilities/File.swift
new file mode 100644
index 00000000..f6b70052
--- /dev/null
+++ b/Sources/SwiftTimecode/Utilities/File.swift
@@ -0,0 +1,94 @@
+//
+// File.swift
+// SwiftTimecode
+//
+// Created by Steffan Andrews on 2020-09-17.
+//
+
+import Foundation
+
+/// CUSTOM SHARED:
+/// Outputs percentage values at defined intervals, from a input stream of values.
+///
+/// `maxValue` must be greater than `minValue`. `progress(value: T)` will not produce output if `value` is less than `minValue`, however `value` > `maxValue` will continue producing outputs > 100%.
+///
+/// Useful in debugging to the console to show incremental progress during operations that can take a fair amount of time to complete.
+public struct SegmentedProgress {
+
+ /// Minimum value.
+ public let minValue: T
+ /// Internal cached value
+ private let dblMinValue: Double
+
+ /// Maximum value.
+ public let maxValue: T
+ /// Internal cached value
+ private let dblMaxValue: Double
+
+ /// How many segments to divide the entire range into.
+ public var segments: Int = 100 {
+ didSet { calculateSegmentInterval() }
+ }
+
+ /// Number of decimal places to round the percentage to.
+ public var roundedToPlaces: Int = 0
+
+ /// Internal, calculated. Do not access directly.
+ private var segmentInterval: Double = 1.0
+
+ /// Internal. Do not access directly.
+ private mutating func calculateSegmentInterval() {
+ if segments < 1 { segments = 1 } // validation
+ segmentInterval = (Double(maxValue) - Double(minValue)) / Double(segments)
+ }
+
+ /// Internal
+ private(set) var lastSegmentValue: Double
+ /// Internal
+ private var firstSegmentIssued = false
+ /// Internal
+ private var lastSegmentIssued = false
+
+ public mutating func progress(value: T) -> String? {
+ if (value == minValue) && !firstSegmentIssued {
+ firstSegmentIssued = true
+ return roundedPercentageString(0.0, toPlaces: roundedToPlaces)
+ }
+
+ if (value == maxValue) && !lastSegmentIssued {
+ lastSegmentIssued = true
+ return roundedPercentageString(100.0, toPlaces: roundedToPlaces)
+ }
+
+ let dblValue = Double(value)
+
+ if dblValue >= lastSegmentValue + segmentInterval {
+ let percentage = ((dblValue - dblMinValue) / (dblMaxValue - dblMinValue)) * 100.0
+
+ lastSegmentValue = dblValue
+ return roundedPercentageString(percentage, toPlaces: roundedToPlaces)
+ }
+
+ return nil
+ }
+
+ public init(_ range: ClosedRange, segments: Int = 100, roundedToPlaces: Int = 0) {
+ self.minValue = range.lowerBound
+ self.dblMinValue = Double(self.minValue)
+
+ self.maxValue = range.upperBound
+ self.dblMaxValue = Double(self.maxValue)
+
+ self.segments = segments
+ self.lastSegmentValue = dblMinValue
+ calculateSegmentInterval() // must call this here since segmets didSet won't trigger from init
+
+ self.roundedToPlaces = roundedToPlaces
+ }
+
+ /// Internal: Utility function.
+ private func roundedPercentageString(_ number: Double, toPlaces: Int) -> String {
+ return String(format: "%.\(toPlaces)f", number) + "%"
+ }
+
+}
diff --git a/Sources/SwiftTimecode/Utilities/FloatingPoint.swift b/Sources/SwiftTimecode/Utilities/FloatingPoint.swift
new file mode 100644
index 00000000..e8c3a7c4
--- /dev/null
+++ b/Sources/SwiftTimecode/Utilities/FloatingPoint.swift
@@ -0,0 +1,151 @@
+//
+// File.swift
+// SwiftTimecode
+//
+// Created by Steffan Andrews on 2020-09-17.
+//
+
+import Foundation
+
+// MARK: - ceiling / floor
+
+extension FloatingPoint {
+
+ /// CUSTOM SHARED: Convenience method to call `ceil()`
+ internal var ceiling: Self {
+ Darwin.ceil(self)
+ }
+
+ /// CUSTOM SHARED: Convenience method to call `floor()`
+ internal var floor: Self {
+ Darwin.floor(self)
+ }
+
+}
+
+// MARK: - FloatingPointPower
+
+/// CUSTOM SHARED:
+/// Protocol allowing implementation of convenience methods for .power(_ exponent:)
+internal protocol FloatingPointPower {
+ func power(_ exponent: Self) -> Self
+}
+
+
+// MARK: - .power()
+
+extension Double: FloatingPointPower {
+ /// CUSTOM SHARED:
+ /// Convenience method for pow()
+ internal func power(_ exponent: Double) -> Double {
+ pow(self, exponent)
+ }
+}
+
+extension Float: FloatingPointPower {
+ /// CUSTOM SHARED:
+ /// Convenience method for pow()
+ internal func power(_ exponent: Float) -> Float {
+ powf(self, exponent)
+ }
+}
+
+// Float80 seems to be deprecated as of the introduction of ARM64
+
+//extension Float80: FloatingPointPower {
+// /// CUSTOM SHARED:
+// /// Convenience method for pow()
+// public func power(_ exponent: Float80) -> Float80 {
+// powl(self, exponent)
+// }
+//}
+
+extension Decimal {
+ /// CUSTOM SHARED:
+ /// Convenience method for pow()
+ internal func power(_ exponent: Int) -> Decimal {
+ pow(self, exponent)
+ }
+}
+
+
+// MARK: - .truncated() / .rounded
+
+extension FloatingPoint where Self : FloatingPointPower {
+
+ /// CUSTOM SHARED:
+ /// Truncates decimal places to `decimalPlaces` number of decimal places. If `decimalPlaces` <= 0, trunc(self) is returned.
+ internal func truncated(decimalPlaces: Int) -> Self {
+ if decimalPlaces < 1 {
+ return trunc(self)
+ }
+
+ let offset = Self(10).power(Self(decimalPlaces))
+ return trunc(self * offset) / offset
+ }
+
+ /// CUSTOM SHARED:
+ /// Replaces this value by truncating it to `decimalPlaces` number of decimal places. If `decimalPlaces` <= 0, trunc(self) is used.
+ internal mutating func formTruncated(decimalPlaces: Int) {
+ self = self.truncated(decimalPlaces: decimalPlaces)
+ }
+
+ /// CUSTOM SHARED:
+ /// Rounds to `decimalPlaces` number of decimal places using rounding `rule`. If `decimalPlaces` <= 0, trunc(self) is returned.
+ internal func rounded(_ rule: FloatingPointRoundingRule = .toNearestOrAwayFromZero, decimalPlaces: Int) -> Self {
+ if decimalPlaces < 1 {
+ return self.rounded(rule)
+ }
+
+ let offset = Self(10).power(Self(decimalPlaces))
+
+ return (self * offset).rounded(rule) / offset
+ }
+
+ /// CUSTOM SHARED:
+ /// Replaces this value by rounding it to `decimalPlaces` number of decimal places using rounding `rule`. If `decimalPlaces` <= 0, trunc(self) is used.
+ internal mutating func round(_ rule: FloatingPointRoundingRule = .toNearestOrAwayFromZero, decimalPlaces: Int) {
+ self = self.rounded(rule, decimalPlaces: decimalPlaces)
+ }
+
+}
+
+extension FloatingPoint {
+
+ /// CUSTOM SHARED:
+ /// Similar to `Int.quotientAndRemainder(dividingBy:)` from the standard Swift library.
+ ///
+ /// Note: Internally, employs `trunc()` and `.truncatingRemainder(dividingBy:)`.
+ internal func quotientAndRemainder(dividingBy: Self) -> (quotient: Self, remainder: Self) {
+ let calculation = (self / dividingBy)
+ let integral = trunc(calculation)
+ let fraction = self.truncatingRemainder(dividingBy: dividingBy)
+ return (quotient: integral, remainder: fraction)
+ }
+
+ /// CUSTOM SHARED:
+ /// Returns both integral part and fractional part.
+ /// This methos is more computationally efficient than calling `.integral` and .`fraction` properties separately unless you only require one or the other.
+ ///
+ /// Note: this can result in a non-trivial loss of precision for the fractional part.
+ internal var integralAndFraction: (integral: Self, fraction: Self) {
+ let integral = trunc(self)
+ let fraction = self - integral
+ return (integral: integral, fraction: fraction)
+ }
+
+ /// CUSTOM SHARED:
+ /// Returns the integral part (digits before the decimal point)
+ internal var integral: Self {
+ integralAndFraction.integral
+ }
+
+ /// CUSTOM SHARED:
+ /// Returns the fractional part (digits after the decimal point)
+ ///
+ /// Note: this can result in a non-trivial loss of precision for the fractional part.
+ internal var fraction: Self {
+ integralAndFraction.fraction
+ }
+
+}
diff --git a/Sources/SwiftTimecode/Utilities/RegEx.swift b/Sources/SwiftTimecode/Utilities/RegEx.swift
new file mode 100644
index 00000000..bd64a1cb
--- /dev/null
+++ b/Sources/SwiftTimecode/Utilities/RegEx.swift
@@ -0,0 +1,75 @@
+//
+// File.swift
+// SwiftTimecode
+//
+// Created by Steffan Andrews on 2020-09-17.
+//
+
+import Foundation
+
+extension String {
+
+ /// CUSTOM SHARED:
+ /// Returns an array of RegEx matches
+ internal func regexMatches(pattern: String) -> [String] {
+ do {
+ let regex = try NSRegularExpression(pattern: pattern)
+ let nsString = self as NSString
+ let results = regex.matches(in: self, range: NSMakeRange(0, nsString.length))
+ return results.map { nsString.substring(with: $0.range)}
+ } catch { // catch let error as NSError {
+ //llog("regexMatches(...) Error: Invalid RegEx: \(error.localizedDescription)")
+ return []
+ }
+ }
+
+ /// CUSTOM SHARED:
+ /// Returns a string from a tokenized string of RegEx matches
+ internal func regexMatches(pattern: String, replacementTemplate: String) -> String? {
+ do {
+ let regex = try NSRegularExpression(pattern: pattern)
+ let nsString = self as NSString
+ regex.numberOfMatches(in: self,
+ options: .withTransparentBounds,
+ range: NSMakeRange(0, nsString.length))
+ let replaced = regex.stringByReplacingMatches(in: self,
+ options: .withTransparentBounds,
+ range: NSMakeRange(0, nsString.length),
+ withTemplate: replacementTemplate)
+
+ return replaced
+ } catch { // catch let error as NSError {
+ return nil
+ }
+ }
+
+ /// CUSTOM SHARED:
+ /// Returns capture groups from regex matches. nil if an optional capture group is not matched.
+ internal func regexMatches(captureGroupsFromPattern: String) -> [String?] {
+ do {
+ let regex = try NSRegularExpression(pattern: captureGroupsFromPattern, options: [])
+ let nsString = self as NSString
+ let results = regex.matches(in: self,
+ options: .withTransparentBounds,
+ range: NSMakeRange(0, nsString.length))
+ var matches: [String?] = []
+
+ for result in results {
+ for i in 1.. NSAttributedString {
+
+ let paragraph = NSMutableParagraphStyle()
+ paragraph.alignment = alignment
+
+ guard let copy = self.mutableCopy() as? NSMutableAttributedString
+ else { return self }
+
+ copy.addAttributes([ .paragraphStyle : paragraph ],
+ range: NSRange(location: 0, length: self.length))
+
+ return copy
+
+ }
+
+}
+
+extension NSMutableAttributedString {
+
+ /// CUSTOM SHARED:
+ /// Convenience. Adds the attribute applied to the entire string.
+ internal func addAttribute(alignment: NSTextAlignment) {
+
+ let paragraph = NSMutableParagraphStyle()
+ paragraph.alignment = alignment
+
+ self.addAttributes([ .paragraphStyle : paragraph ],
+ range: NSRange(location: 0, length: self.length))
+
+ }
+
+}
diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift
new file mode 100644
index 00000000..3e7c3466
--- /dev/null
+++ b/Tests/LinuxMain.swift
@@ -0,0 +1,4 @@
+// LinuxMain implementation is no longer required as of Swift 5.1.
+// see https://oleb.net/2020/swift-test-discovery/
+
+fatalError("Run the tests with `swift test --enable-test-discovery`")
diff --git a/Tests/SwiftTimecode-Dev-Tests/OTTimecode Elapsed Frames ExtendedTests.swift b/Tests/SwiftTimecode-Dev-Tests/OTTimecode Elapsed Frames ExtendedTests.swift
new file mode 100644
index 00000000..ecb88a56
--- /dev/null
+++ b/Tests/SwiftTimecode-Dev-Tests/OTTimecode Elapsed Frames ExtendedTests.swift
@@ -0,0 +1,110 @@
+//
+// OTTimecode Elapsed Frames ExtendedTests.swift
+// OTTimecodeExtendedTests
+//
+// Created by Steffan Andrews on 2020-06-16.
+// Copyright © 2020 Steffan Andrews. All rights reserved.
+//
+
+import XCTest
+@testable import SwiftTimecode
+
+class OTTimecode_ET_ExtendedTests: XCTestCase {
+
+ func testOTTimecode_Iterative() {
+
+ // return early to bypass this dev-only test
+
+ XCTAssertTrue(true)
+ return
+
+ // test conversions from components(from:) and totalElapsedFrames(of:)
+
+ // ==============================================================================
+ // NOTE:
+ // ==============================================================================
+ // this is a brute-force test not meant to be run frequently,
+ // but as a diagnostic testbed only when major changes are made to the library
+ // to ensure that conversions are accurate
+ // ==============================================================================
+
+ // ======= parameters =======
+
+ let limit: OTTimecode.UpperLimit = ._24hours
+// let limit: OTTimecode.UpperLimit = ._100days
+
+ let frameRatesToTest: [OTTimecode.FrameRate] = OTTimecode.FrameRate.allCases
+// let frameRatesToTest: [OTTimecode.FrameRate] = OTTimecode.FrameRate.allDrop
+// let frameRatesToTest: [OTTimecode.FrameRate] = OTTimecode.FrameRate.allNonDrop
+// let frameRatesToTest: [OTTimecode.FrameRate] = [._60_drop, ._120_drop]
+
+ // ======= run ==============
+
+ for fr in frameRatesToTest {
+
+ let tc = OTTimecode(at: fr, limit: limit)
+
+ // log status
+ print ("Testing all frames in \(tc.upperLimit) at \(fr.stringValue)... ", terminator: "")
+
+ var failures: [(Int, TCC)] = []
+
+ let ubound = tc.frameRate.maxTotalFrames(in: tc.upperLimit)
+
+ var per = SegmentedProgress(0...ubound, segments: 20, roundedToPlaces: 0)
+
+ for i in 0...ubound {
+ let vals = OTTimecode.components(from: i,
+ at: tc.frameRate,
+ subFramesDivisor: tc.subFramesDivisor)
+
+ if i != Int(floor(OTTimecode.totalElapsedFrames(of: vals, at: tc.frameRate,
+ subFramesDivisor: tc.subFramesDivisor)))
+
+ { failures.append((i, vals)) }
+
+ // log status
+ if let percentageToPrint = per.progress(value: i) {
+ print("\(percentageToPrint) ", terminator: "")
+ }
+ }
+ print("") // finalize log with newline char
+
+ XCTAssertEqual(failures.count, 0, "Failed iterative test for \(fr) with \(failures.count) failures.")
+
+ if failures.count > 0 {
+ print("First",
+ fr,
+ "failure: input elapsed frames",
+ failures.first!.0,
+ "converted to components",
+ failures.first!.1,
+ "converted back to",
+ OTTimecode.totalElapsedFrames(of: failures.first!.1,
+ at: tc.frameRate,
+ subFramesDivisor: tc.subFramesDivisor),
+ "elapsed frames.")
+
+ }
+ if failures.count > 1 {
+ print("Last",
+ fr,
+ "failure: input elapsed frames",
+ failures.last!.0,
+ "converted to components",
+ failures.last!.1,
+ "converted back to",
+ OTTimecode.totalElapsedFrames(of: failures.last!.1,
+ at: tc.frameRate,
+ subFramesDivisor: tc.subFramesDivisor),
+ "elapsed frames.")
+
+ }
+
+ }
+
+ print("Done")
+
+ }
+
+}
diff --git a/Tests/SwiftTimecode-Unit-Tests/Integration Tests/OTTimecode Integration Tests.swift b/Tests/SwiftTimecode-Unit-Tests/Integration Tests/OTTimecode Integration Tests.swift
new file mode 100644
index 00000000..cb808a1f
--- /dev/null
+++ b/Tests/SwiftTimecode-Unit-Tests/Integration Tests/OTTimecode Integration Tests.swift
@@ -0,0 +1,124 @@
+//
+// SwiftTimecodeTests.swift
+// SwiftTimecodeTests
+//
+// Created by Steffan Andrews on 2019-09-30.
+// Copyright © 2019 Steffan Andrews. All rights reserved.
+//
+
+import XCTest
+@testable import SwiftTimecode
+
+class OTTimecode_IT_IntegrationTests: XCTestCase {
+
+ func testOTTimecode_Clamping() {
+
+ // 24 hour
+
+ OTTimecode.FrameRate.allCases.forEach {
+
+ XCTAssertEqual(OTTimecode(clamping: TCC(h: -1, m: -1, s: -1, f: -1), at: $0).components,
+ TCC(d: 0, h: 0, m: 0, s: 0, f: 0),
+ "for \($0)")
+
+ }
+
+ OTTimecode.FrameRate.allCases.forEach {
+ let clamped = OTTimecode(clamping: TCC(h: 99, m: 99, s: 99, f: 10000), at: $0).components
+
+ XCTAssertEqual(clamped, TCC(d: 0, h: 23, m: 59, s: 59, f: $0.maxFrameNumberDisplayable), "for \($0)")
+ }
+
+ // 24 hour - testing with days
+
+ OTTimecode.FrameRate.allCases.forEach {
+
+ XCTAssertEqual(OTTimecode(clamping: TCC(d: -1, h: -1, m: -1, s: -1, f: -1), at: $0).components,
+ TCC(d: 0, h: 0, m: 0, s: 0, f: 0),
+ "for \($0)")
+
+ }
+
+ OTTimecode.FrameRate.allCases.forEach {
+ let clamped = OTTimecode(clamping: TCC(d: 99, h: 99, m: 99, s: 99, f: 10000), at: $0).components
+
+ XCTAssertEqual(clamped, TCC(d: 0, h: 23, m: 59, s: 59, f: $0.maxFrameNumberDisplayable), "for \($0)")
+
+ }
+
+ // 100 days
+
+ OTTimecode.FrameRate.allCases.forEach {
+
+ XCTAssertEqual(OTTimecode(clamping: TCC(h: -1, m: -1, s: -1, f: -1), at: $0).components,
+ TCC(d: 0, h: 0, m: 0, s: 0, f: 0),
+ "for \($0)")
+
+ }
+
+ OTTimecode.FrameRate.allCases.forEach {
+ let clamped = OTTimecode(clamping: TCC(h: 99, m: 99, s: 99, f: 10000), at: $0).components
+
+ XCTAssertEqual(clamped, TCC(d: 0, h: 23, m: 59, s: 59, f: $0.maxFrameNumberDisplayable), "for \($0)")
+
+ }
+
+ // 100 days - testing with days
+
+ OTTimecode.FrameRate.allCases.forEach {
+
+ XCTAssertEqual(OTTimecode(clamping: TCC(d: -1, h: -1, m: -1, s: -1, f: -1), at: $0, limit: ._100days).components,
+ TCC(d: 0, h: 0, m: 0, s: 0, f: 0),
+ "for \($0)")
+
+ }
+
+ OTTimecode.FrameRate.allCases.forEach {
+ let clamped = OTTimecode(clamping: TCC(d: 99, h: 99, m: 99, s: 99, f: 10000), at: $0, limit: ._100days).components
+
+ XCTAssertEqual(clamped, TCC(d: 99, h: 23, m: 59, s: 59, f: $0.maxFrameNumberDisplayable), "for \($0)")
+
+ }
+
+ }
+
+ func testOTTimecode_Wrapping() {
+
+ // 24 hour
+
+ OTTimecode.FrameRate.allCases.forEach {
+
+ XCTAssertEqual(OTTimecode(wrapping: TCC(d: 1), at: $0).components,
+ TCC(d: 0, h: 0, m: 0, s: 0, f: 0),
+ "for \($0)")
+
+ }
+
+ OTTimecode.FrameRate.allCases.forEach {
+ let wrapped = OTTimecode(wrapping: TCC(f: -1), at: $0).components
+
+ XCTAssertEqual(wrapped, TCC(d: 0, h: 23, m: 59, s: 59, f: $0.maxFrameNumberDisplayable, sf: 0), "for \($0)")
+
+ }
+
+ // 24 hour - testing with days
+
+ OTTimecode.FrameRate.allCases.forEach {
+ let wrapped = OTTimecode(wrapping: TCC(d: 1, h: 2, m: 30, s: 20, f: 0), at: $0).components
+
+ XCTAssertEqual(wrapped, TCC(d: 0, h: 2, m: 30, s: 20, f: 0), "for \($0)")
+
+ }
+
+ // 100 days
+
+ OTTimecode.FrameRate.allCases.forEach {
+ let wrapped = OTTimecode(wrapping: TCC(d: -1), at: $0, limit: ._100days).components
+
+ XCTAssertEqual(wrapped, TCC(d: 99, h: 0, m: 0, s: 0, f: 0), "for \($0)")
+
+ }
+
+ }
+
+}
diff --git a/Tests/SwiftTimecode-Unit-Tests/Unit Tests/Components/Components Tests.swift b/Tests/SwiftTimecode-Unit-Tests/Unit Tests/Components/Components Tests.swift
new file mode 100644
index 00000000..53d43006
--- /dev/null
+++ b/Tests/SwiftTimecode-Unit-Tests/Unit Tests/Components/Components Tests.swift
@@ -0,0 +1,24 @@
+//
+// Components Tests.swift
+// SwiftTimecodeTests
+//
+// Created by Steffan Andrews on 2020-06-16.
+// Copyright © 2020 Steffan Andrews. All rights reserved.
+//
+
+import XCTest
+@testable import SwiftTimecode
+
+class OTTimecode_UT_Components_Tests: XCTestCase {
+
+ override func setUp() { }
+ override func tearDown() { }
+
+ func testTCC_toTimecode() {
+
+ XCTAssertEqual(TCC(d: 0, h: 1, m: 2, s: 3, f: 4, sf: 0).toTimecode(at: ._23_976),
+ OTTimecode(TCC(h: 1, m: 2, s: 3, f: 4), at: ._23_976))
+
+ }
+
+}
diff --git a/Tests/SwiftTimecode-Unit-Tests/Unit Tests/Data Interchange/OTTimecode Components Tests.swift b/Tests/SwiftTimecode-Unit-Tests/Unit Tests/Data Interchange/OTTimecode Components Tests.swift
new file mode 100644
index 00000000..4dc5c8de
--- /dev/null
+++ b/Tests/SwiftTimecode-Unit-Tests/Unit Tests/Data Interchange/OTTimecode Components Tests.swift
@@ -0,0 +1,142 @@
+//
+// OTTimecode Components Tests.swift
+// OTTimecodeUnitTests
+//
+// Created by Steffan Andrews on 2020-06-17.
+// Copyright © 2020 Steffan Andrews. All rights reserved.
+//
+
+import XCTest
+@testable import SwiftTimecode
+
+class OTTimecode_UT_DI_Components_Tests: XCTestCase {
+
+ override func setUp() { }
+ override func tearDown() { }
+
+ func testOTTimecode_components_24hours() {
+
+ // default
+
+ var tc = OTTimecode(at: ._30)
+
+ XCTAssertEqual(tc.days , 0)
+ XCTAssertEqual(tc.hours , 0)
+ XCTAssertEqual(tc.minutes , 0)
+ XCTAssertEqual(tc.seconds , 0)
+ XCTAssertEqual(tc.frames , 0)
+ XCTAssertEqual(tc.subFrames , 0)
+
+ // setter
+
+ tc.components = TCC(h: 1, m: 2, s: 3, f: 4, sf: 5)
+
+ XCTAssertEqual(tc.days , 0)
+ XCTAssertEqual(tc.hours , 1)
+ XCTAssertEqual(tc.minutes , 2)
+ XCTAssertEqual(tc.seconds , 3)
+ XCTAssertEqual(tc.frames , 4)
+ XCTAssertEqual(tc.subFrames , 5)
+
+ // getter
+
+ let c = tc.components
+
+ XCTAssertEqual(c.d , 0)
+ XCTAssertEqual(c.h , 1)
+ XCTAssertEqual(c.m , 2)
+ XCTAssertEqual(c.s , 3)
+ XCTAssertEqual(c.f , 4)
+ XCTAssertEqual(c.sf , 5)
+
+ }
+
+ func testOTTimecode_components_100days() {
+
+ // default
+
+ var tc = OTTimecode(at: ._30, limit: ._100days)
+
+ XCTAssertEqual(tc.days , 0)
+ XCTAssertEqual(tc.hours , 0)
+ XCTAssertEqual(tc.minutes , 0)
+ XCTAssertEqual(tc.seconds , 0)
+ XCTAssertEqual(tc.frames , 0)
+ XCTAssertEqual(tc.subFrames , 0)
+
+ // setter
+
+ tc.components = TCC(d: 5, h: 1, m: 2, s: 3, f: 4, sf: 5)
+
+ XCTAssertEqual(tc.days , 5)
+ XCTAssertEqual(tc.hours , 1)
+ XCTAssertEqual(tc.minutes , 2)
+ XCTAssertEqual(tc.seconds , 3)
+ XCTAssertEqual(tc.frames , 4)
+ XCTAssertEqual(tc.subFrames , 5)
+
+ // getter
+
+ let c = tc.components
+
+ XCTAssertEqual(c.d , 5)
+ XCTAssertEqual(c.h , 1)
+ XCTAssertEqual(c.m , 2)
+ XCTAssertEqual(c.s , 3)
+ XCTAssertEqual(c.f , 4)
+ XCTAssertEqual(c.sf , 5)
+
+ }
+
+ func testSetTimecodeExactly() {
+
+ // this is not meant to test the underlying logic, simply that .setTimecode produces the intended outcome
+
+ var tc = OTTimecode(at: ._30)
+
+ tc.setTimecode(exactly: TCC(h: 1, m: 2, s: 3, f: 4, sf: 5))
+
+ XCTAssertEqual(tc.days , 0)
+ XCTAssertEqual(tc.hours , 1)
+ XCTAssertEqual(tc.minutes , 2)
+ XCTAssertEqual(tc.seconds , 3)
+ XCTAssertEqual(tc.frames , 4)
+ XCTAssertEqual(tc.subFrames , 5)
+
+ }
+
+ func testSetTimecodeClamping() {
+
+ // this is not meant to test the underlying logic, simply that .setTimecode produces the intended outcome
+
+ var tc = OTTimecode(at: ._30)
+
+ tc.setTimecode(clamping: TCC(d: 1, h: 70, m: 70, s: 70, f: 70, sf: 500))
+
+ XCTAssertEqual(tc.days , 0)
+ XCTAssertEqual(tc.hours , 23)
+ XCTAssertEqual(tc.minutes , 59)
+ XCTAssertEqual(tc.seconds , 59)
+ XCTAssertEqual(tc.frames , 29)
+ XCTAssertEqual(tc.subFrames , 79)
+
+ }
+
+ func testSetTimecodeWrapping() {
+
+ // this is not meant to test the underlying logic, simply that .setTimecode produces the intended outcome
+
+ var tc = OTTimecode(at: ._30)
+
+ tc.setTimecode(wrapping: TCC(f: -1))
+
+ XCTAssertEqual(tc.days , 0)
+ XCTAssertEqual(tc.hours , 23)
+ XCTAssertEqual(tc.minutes , 59)
+ XCTAssertEqual(tc.seconds , 59)
+ XCTAssertEqual(tc.frames , 29)
+ XCTAssertEqual(tc.subFrames , 0)
+
+ }
+
+}
diff --git a/Tests/SwiftTimecode-Unit-Tests/Unit Tests/Data Interchange/OTTimecode Real Time Tests.swift b/Tests/SwiftTimecode-Unit-Tests/Unit Tests/Data Interchange/OTTimecode Real Time Tests.swift
new file mode 100644
index 00000000..eb9c8509
--- /dev/null
+++ b/Tests/SwiftTimecode-Unit-Tests/Unit Tests/Data Interchange/OTTimecode Real Time Tests.swift
@@ -0,0 +1,151 @@
+//
+// OTTimecode Real Time Tests.swift
+// OTTimecodeUnitTests
+//
+// Created by Steffan Andrews on 2020-06-16.
+// Copyright © 2020 Steffan Andrews. All rights reserved.
+//
+
+import XCTest
+@testable import SwiftTimecode
+
+class OTTimecode_UT_DI_Real_Time_Tests: XCTestCase {
+
+ override func setUp() { }
+ override func tearDown() { }
+
+ func testOTTimecode_RealTime() {
+
+ // pre-computed constants
+
+ let msIn10Hr_ShrunkFrameRates = 864864000.0
+ let msIn10Hr_BaseFrameRates = 864000000.0
+ let msIn10Hr_DropFrameRates = 863999136.0
+
+
+ // get real time
+
+ // allow for the over-estimate padding value that gets added in the TC->realtime method
+ let accuracy = 0.0001
+
+ OTTimecode.FrameRate.allCases.forEach {
+
+ let tc = OTTimecode(TCC(d: 10), at: $0, limit: ._100days)!
+
+ switch $0 {
+ case ._23_976,
+ ._24_98,
+ ._29_97,
+ ._47_952,
+ ._59_94,
+ ._119_88:
+
+ XCTAssertEqual(tc.realTime.ms, msIn10Hr_ShrunkFrameRates, accuracy: accuracy, "at: \($0)")
+
+ case ._24,
+ ._25,
+ ._30,
+ ._48,
+ ._50,
+ ._60,
+ ._100,
+ ._120:
+
+ XCTAssertEqual(tc.realTime.ms, msIn10Hr_BaseFrameRates, accuracy: accuracy, "at: \($0)")
+
+ case ._29_97_drop,
+ ._30_drop,
+ ._59_94_drop,
+ ._60_drop,
+ ._119_88_drop,
+ ._120_drop:
+
+ XCTAssertEqual(tc.realTime.ms, msIn10Hr_DropFrameRates, accuracy: accuracy, "at: \($0)")
+
+ }
+ }
+
+ // set timecode from real time
+
+ let tcc = TCC(d: 10)
+
+ OTTimecode.FrameRate.allCases.forEach {
+
+ var tc = OTTimecode(tcc, at: $0, limit: ._100days)!
+
+ switch $0 {
+ case ._23_976,
+ ._24_98,
+ ._29_97,
+ ._47_952,
+ ._59_94,
+ ._119_88:
+
+ XCTAssertTrue(tc.setTimecode(from: OTTime(ms: msIn10Hr_ShrunkFrameRates)), "at: \($0)")
+ XCTAssertEqual(tc.components, tcc, "at: \($0)")
+
+ case ._24,
+ ._25,
+ ._30,
+ ._48,
+ ._50,
+ ._60,
+ ._100,
+ ._120:
+
+ XCTAssertTrue(tc.setTimecode(from: OTTime(ms: msIn10Hr_BaseFrameRates)), "at: \($0)")
+ XCTAssertEqual(tc.components, tcc, "at: \($0)")
+
+ case ._29_97_drop,
+ ._30_drop,
+ ._59_94_drop,
+ ._60_drop,
+ ._119_88_drop,
+ ._120_drop:
+
+ XCTAssertTrue(tc.setTimecode(from: OTTime(ms: msIn10Hr_DropFrameRates)), "at: \($0)")
+ XCTAssertEqual(tc.components, tcc, "at: \($0)")
+
+ }
+
+ }
+
+ }
+
+ func testOTTimecode_RealTime_SubFrames() {
+
+ // ensure subframes are calculated correctly
+
+ // test for precision and rounding issues by iterating every subframe for each frame rate
+
+ for subframe in 0...79 {
+
+ let tcc = TCC(d: 99, h: 23, sf: subframe)
+
+ OTTimecode.FrameRate.allCases.forEach {
+
+ var tc = OTTimecode(tcc,
+ at: $0,
+ limit: ._100days,
+ subFramesDivisor: 80)!
+
+ // timecode to samples
+
+ let realTime = tc.realTime
+
+ // samples to timecode
+
+ XCTAssertTrue(tc.setTimecode(from: realTime),
+ "at: \($0) subframe: \(subframe)")
+
+ XCTAssertEqual(tc.components,
+ tcc,
+ "at: \($0) subframe: \(subframe)")
+
+ }
+
+ }
+
+ }
+
+}
diff --git a/Tests/SwiftTimecode-Unit-Tests/Unit Tests/Data Interchange/OTTimecode Samples Tests.swift b/Tests/SwiftTimecode-Unit-Tests/Unit Tests/Data Interchange/OTTimecode Samples Tests.swift
new file mode 100644
index 00000000..0752e9c9
--- /dev/null
+++ b/Tests/SwiftTimecode-Unit-Tests/Unit Tests/Data Interchange/OTTimecode Samples Tests.swift
@@ -0,0 +1,164 @@
+//
+// OTTimecode Samples Tests.swift
+// OTTimecodeUnitTests
+//
+// Created by Steffan Andrews on 2020-06-16.
+// Copyright © 2020 Steffan Andrews. All rights reserved.
+//
+
+import XCTest
+@testable import SwiftTimecode
+
+class OTTimecode_UT_DI_Samples_Tests: XCTestCase {
+
+ override func setUp() { }
+ override func tearDown() { }
+
+ func testSamplesGetSet_48KHz() {
+
+ // pre-computed constants
+
+ let samplesIn1DayTC_ShrunkFrameRates = 4151347200.0
+ let samplesIn1DayTC_BaseFrameRates = 4147200000.0
+ let samplesIn1DayTC_DropFrameRates = 4147195853.0
+
+ let samplesIn1DayTC_30DF = 4143052800.0 // confirmed correct in PT and Cubase
+
+ // allow for the over-estimate padding value that gets added in the TC->samples method
+ let accuracy = 0.001
+
+ // 48KHz ___________________________________
+
+ OTTimecode.FrameRate.allCases.forEach {
+
+ let sRate = 48000
+ var tc = OTTimecode(at: $0, limit: ._100days)
+
+ switch $0 {
+ case ._23_976,
+ ._24_98,
+ ._29_97,
+ ._47_952,
+ ._59_94,
+ ._119_88:
+
+ // get
+ tc.setTimecode(exactly: TCC(d: 1))
+ XCTAssertEqual(tc.samplesValue(atSampleRate: sRate),
+ samplesIn1DayTC_ShrunkFrameRates,
+ accuracy: accuracy,
+ "at \($0)")
+
+ // set
+ tc.setTimecode(fromSamplesValue: samplesIn1DayTC_ShrunkFrameRates,
+ atSampleRate: sRate)
+ XCTAssertEqual(tc.components,
+ TCC(d: 1),
+ "at \($0)")
+
+ case ._24,
+ ._25,
+ ._30,
+ ._48,
+ ._50,
+ ._60,
+ ._100,
+ ._120:
+
+ // get
+ tc.setTimecode(exactly: TCC(d: 1))
+ XCTAssertEqual(tc.samplesValue(atSampleRate: sRate),
+ samplesIn1DayTC_BaseFrameRates,
+ accuracy: accuracy,
+ "at \($0)")
+
+ // set
+ tc.setTimecode(fromSamplesValue: samplesIn1DayTC_BaseFrameRates,
+ atSampleRate: sRate)
+ XCTAssertEqual(tc.components,
+ TCC(d: 1),
+ "at \($0)")
+
+ case ._29_97_drop,
+ ._59_94_drop,
+ ._119_88_drop:
+
+ // Cubase reports 4147195853 @ 1 day - there may be rounding happening in Cubase
+ // Pro Tools reports 2073597926 @ 12 hours; double this would technically be 4147195854 but Cubase shows 1 frame less
+
+ // get
+ tc.setTimecode(exactly: TCC(d: 1))
+ XCTAssertEqual(tc.samplesValue(atSampleRate: sRate).rounded(), // add rounding for dropframe; DAWs seem to round using standard rounding rules (?)
+ samplesIn1DayTC_DropFrameRates,
+ "at \($0)")
+
+ // set
+ tc.setTimecode(fromSamplesValue: samplesIn1DayTC_DropFrameRates,
+ atSampleRate: sRate)
+ XCTAssertEqual(tc.components, TCC(d: 1),
+ "at \($0)")
+
+ case ._30_drop,
+ ._60_drop,
+ ._120_drop:
+
+ // get
+ tc.setTimecode(exactly: TCC(d: 1))
+ XCTAssertEqual(tc.samplesValue(atSampleRate: sRate),
+ samplesIn1DayTC_30DF,
+ accuracy: accuracy,
+ "at \($0)")
+
+ // set
+ tc.setTimecode(fromSamplesValue: samplesIn1DayTC_30DF,
+ atSampleRate: sRate)
+ XCTAssertEqual(tc.components,
+ TCC(d: 1),
+ "at \($0)")
+
+ }
+
+ }
+
+ }
+
+ func testOTTimecode_Samples_SubFrames() {
+
+ // ensure subframes are calculated correctly
+
+ // test for precision and rounding issues by iterating every subframe for each frame rate
+
+ for subframe in 0...79 {
+
+ let tcc = TCC(d: 99, h: 23, sf: subframe)
+
+ OTTimecode.FrameRate.allCases.forEach {
+
+ var tc = OTTimecode(tcc,
+ at: $0,
+ limit: ._100days,
+ subFramesDivisor: 80)!
+
+ let sRate = 48000
+
+ // timecode to samples
+
+ let samples = tc.samplesValue(atSampleRate: sRate)
+
+ // samples to timecode
+
+ XCTAssertTrue(tc.setTimecode(fromSamplesValue: samples,
+ atSampleRate: sRate),
+ "at: \($0) subframe: \(subframe)")
+
+ XCTAssertEqual(tc.components,
+ tcc,
+ "at: \($0) subframe: \(subframe)")
+
+ }
+
+ }
+
+ }
+
+}
diff --git a/Tests/SwiftTimecode-Unit-Tests/Unit Tests/Data Interchange/OTTimecode String Tests.swift b/Tests/SwiftTimecode-Unit-Tests/Unit Tests/Data Interchange/OTTimecode String Tests.swift
new file mode 100644
index 00000000..1c0190a2
--- /dev/null
+++ b/Tests/SwiftTimecode-Unit-Tests/Unit Tests/Data Interchange/OTTimecode String Tests.swift
@@ -0,0 +1,335 @@
+//
+// OTTimecode String Tests.swift
+// SwiftTimecodeTests
+//
+// Created by Steffan Andrews on 2020-06-16.
+// Copyright © 2020 Steffan Andrews. All rights reserved.
+//
+
+import XCTest
+@testable import SwiftTimecode
+
+class OTTimecode_UT_DI_String_Tests: XCTestCase {
+
+ override func setUp() { }
+ override func tearDown() { }
+
+ func testStringValue_GetSet_Basic() {
+
+ // basic set & get tests
+
+ var tc = OTTimecode(at: ._23_976, limit: ._24hours)
+
+ tc.stringValue = "01:05:20:14"
+ XCTAssertEqual(tc.stringValue, "01:05:20:14")
+
+ tc.stringValue = "50:05:20:14" // fails silently
+ XCTAssertEqual(tc.stringValue, "01:05:20:14") // old value
+
+ XCTAssertFalse(tc.setTimecode(exactly: "50:05:20:14"))
+ XCTAssertEqual(tc.stringValue, "01:05:20:14") // no change
+
+ XCTAssertTrue(tc.setTimecode(clamping: "50:05:20:14"))
+ XCTAssertEqual(tc.stringValue, "23:05:20:14")
+
+ }
+
+ func testStringValue_Get_Formatting_Basic() {
+
+ // basic string formatting - ie: HH:MM:SS:FF
+ // using known valid timecode components; not testing for invalid values here
+
+ // 24 hour limit
+
+ // non-drop
+
+ OTTimecode.FrameRate.allNonDrop.forEach {
+ let sv = TCC(h: 1, m: 02, s: 03, f: 04)
+ .toTimecode(at: $0)?
+ .stringValue
+
+ let t = $0.numberOfDigits == 2 ? "" : "0"
+
+ XCTAssertEqual(sv, "01:02:03:\(t)04", "for \($0)")
+ }
+
+ // drop
+
+ OTTimecode.FrameRate.allDrop.forEach {
+ let sv = TCC(h: 1, m: 02, s: 03, f: 04)
+ .toTimecode(at: $0)?
+ .stringValue
+
+ let t = $0.numberOfDigits == 2 ? "" : "0"
+
+ XCTAssertEqual(sv, "01:02:03;\(t)04", "for \($0)")
+ }
+
+ // 100 days limit
+
+ // non-drop
+
+ OTTimecode.FrameRate.allNonDrop.forEach {
+ let sv = TCC(h: 1, m: 02, s: 03, f: 04)
+ .toTimecode(at: $0, limit: ._100days)?
+ .stringValue
+
+ let t = $0.numberOfDigits == 2 ? "" : "0"
+
+ XCTAssertEqual(sv, "01:02:03:\(t)04", "for \($0)") // omits days since they are 0
+ }
+
+ // drop
+
+ OTTimecode.FrameRate.allDrop.forEach {
+ let sv = TCC(h: 1, m: 02, s: 03, f: 04)
+ .toTimecode(at: $0, limit: ._100days)?
+ .stringValue
+
+ let t = $0.numberOfDigits == 2 ? "" : "0"
+
+ XCTAssertEqual(sv, "01:02:03;\(t)04", "for \($0)") // omits days since they are 0
+ }
+ }
+
+ func testStringValue_Get_Formatting_WithDays() {
+
+ // string formatting with days - ie: D:HH:MM:SS:FF
+ // using known valid timecode components; not testing for invalid values here
+
+ // non-drop
+
+ OTTimecode.FrameRate.allNonDrop.forEach {
+ var tc = TCC(h: 1, m: 02, s: 03, f: 04)
+ .toTimecode(at: $0)
+ tc?.days = 2 // set days after init since init fails if we pass days
+
+ let t = $0.numberOfDigits == 2 ? "" : "0"
+
+ // still produces days since we have not clamped it yet
+ var sv = tc?.stringValue
+ XCTAssertEqual(sv, "2 01:02:03:\(t)04", "for \($0)")
+
+ // now omits days since our limit is 24hr and clamped
+ tc!.clampComponents()
+ sv = tc?.stringValue
+ XCTAssertEqual(sv, "01:02:03:\(t)04", "for \($0)")
+ }
+
+ // drop
+
+ OTTimecode.FrameRate.allDrop.forEach {
+ var tc = TCC(h: 1, m: 02, s: 03, f: 04)
+ .toTimecode(at: $0)
+ tc?.days = 2 // set days after init since init fails if we pass days
+
+ let t = $0.numberOfDigits == 2 ? "" : "0"
+
+ // still produces days since we have not clamped it yet
+ var sv = tc?.stringValue
+ XCTAssertEqual(sv, "2 01:02:03;\(t)04", "for \($0)")
+
+ // now omits days since our limit is 24hr and clamped
+ tc?.clampComponents()
+ sv = tc?.stringValue
+ XCTAssertEqual(sv, "01:02:03;\(t)04", "for \($0)")
+ }
+
+ // 100 days limit
+
+ // non-drop
+
+ OTTimecode.FrameRate.allNonDrop.forEach {
+ let sv = TCC(d: 2, h: 1, m: 02, s: 03, f: 04)
+ .toTimecode(at: $0, limit: ._100days)?
+ .stringValue
+
+ let t = $0.numberOfDigits == 2 ? "" : "0"
+
+ XCTAssertEqual(sv, "2 01:02:03:\(t)04", "for \($0)") // omits days since they are 0
+ }
+
+ // drop
+
+ OTTimecode.FrameRate.allDrop.forEach {
+ let sv = TCC(d: 2, h: 1, m: 02, s: 03, f: 04)
+ .toTimecode(at: $0, limit: ._100days)?
+ .stringValue
+
+ let t = $0.numberOfDigits == 2 ? "" : "0"
+
+ XCTAssertEqual(sv, "2 01:02:03;\(t)04", "for \($0)") // omits days since they are 0
+ }
+
+ }
+
+ func testStringValue_Get_Formatting_WithSubframes() {
+
+ // string formatting with subframes - ie: HH:MM:SS:FF.sf
+ // using known valid timecode components; not testing for invalid values here
+
+ // non-drop
+
+ OTTimecode.FrameRate.allNonDrop.forEach {
+ var tc = TCC(h: 1, m: 02, s: 03, f: 04, sf: 12)
+ .toTimecode(at: $0)
+ tc?.displaySubFrames = true
+ tc?.days = 2 // set days after init since init fails if we pass days
+
+ let t = $0.numberOfDigits == 2 ? "" : "0"
+
+ // still produces days since we have not clamped it yet
+ var sv = tc?.stringValue
+ XCTAssertEqual(sv, "2 01:02:03:\(t)04.12", "for \($0)")
+
+ // now omits days since our limit is 24hr and clamped
+ tc!.clampComponents()
+ sv = tc?.stringValue
+ XCTAssertEqual(sv, "01:02:03:\(t)04.12", "for \($0)")
+ }
+
+ // drop
+
+ OTTimecode.FrameRate.allDrop.forEach {
+ var tc = TCC(h: 1, m: 02, s: 03, f: 04, sf: 12)
+ .toTimecode(at: $0)
+ tc?.displaySubFrames = true
+ tc?.days = 2 // set days after init since init fails if we pass days
+
+ let t = $0.numberOfDigits == 2 ? "" : "0"
+
+ // still produces days since we have not clamped it yet
+ var sv = tc?.stringValue
+ XCTAssertEqual(sv, "2 01:02:03;\(t)04.12", "for \($0)")
+
+ // now omits days since our limit is 24hr and clamped
+ tc?.clampComponents()
+ sv = tc?.stringValue
+ XCTAssertEqual(sv, "01:02:03;\(t)04.12", "for \($0)")
+ }
+
+ // 100 days limit
+
+ // non-drop
+
+ OTTimecode.FrameRate.allNonDrop.forEach {
+ var tc = TCC(d: 2, h: 1, m: 02, s: 03, f: 04, sf: 12)
+ .toTimecode(at: $0, limit: ._100days)
+ tc?.displaySubFrames = true
+
+ let t = $0.numberOfDigits == 2 ? "" : "0"
+
+ let sv = tc?.stringValue
+ XCTAssertEqual(sv, "2 01:02:03:\(t)04.12", "for \($0)") // omits days since they are 0
+ }
+
+ // drop
+
+ OTTimecode.FrameRate.allDrop.forEach {
+ var tc = TCC(d: 2, h: 1, m: 02, s: 03, f: 04, sf: 12)
+ .toTimecode(at: $0, limit: ._100days)
+ tc?.displaySubFrames = true
+
+ let t = $0.numberOfDigits == 2 ? "" : "0"
+
+ let sv = tc?.stringValue
+ XCTAssertEqual(sv, "2 01:02:03;\(t)04.12", "for \($0)") // omits days since they are 0
+ }
+
+ }
+
+ func testStringDecode() {
+
+ // non-drop frame
+
+ XCTAssertNil( OTTimecode.decode(timecode: ""))
+ XCTAssertNil( OTTimecode.decode(timecode: "01564523"))
+ XCTAssertEqual(OTTimecode.decode(timecode: "0:0:0:0"),
+ TCC(d: 0, h: 00, m: 00, s: 00, f: 00, sf: 00))
+
+ XCTAssertEqual(OTTimecode.decode(timecode: "0:00:00:00"),
+ TCC(d: 0, h: 00, m: 00, s: 00, f: 00, sf: 00))
+
+ XCTAssertEqual(OTTimecode.decode(timecode: "00:00:00:00"),
+ TCC(d: 0, h: 00, m: 00, s: 00, f: 00, sf: 00))
+
+ XCTAssertEqual(OTTimecode.decode(timecode: "1:56:45:23"),
+ TCC(d: 0, h: 01, m: 56, s: 45, f: 23, sf: 00))
+
+ XCTAssertEqual(OTTimecode.decode(timecode: "01:56:45:23"),
+ TCC(d: 0, h: 01, m: 56, s: 45, f: 23, sf: 00))
+
+ XCTAssertEqual(OTTimecode.decode(timecode: "3 01:56:45:23"),
+ TCC(d: 3, h: 01, m: 56, s: 45, f: 23, sf: 00))
+
+ XCTAssertEqual(OTTimecode.decode(timecode: "12 01:56:45:23"),
+ TCC(d: 12, h: 1, m: 56, s: 45, f: 23, sf: 00))
+
+ XCTAssertEqual(OTTimecode.decode(timecode: "12:01:56:45:23"),
+ TCC(d: 12, h: 1, m: 56, s: 45, f: 23, sf: 00))
+
+ // drop frame
+
+ XCTAssertEqual(OTTimecode.decode(timecode: "0:0:0;0"),
+ TCC(d: 0, h: 00, m: 00, s: 00, f: 00, sf: 00))
+
+ XCTAssertEqual(OTTimecode.decode(timecode: "0:00:00;00"),
+ TCC(d: 0, h: 00, m: 00, s: 00, f: 00, sf: 00))
+
+ XCTAssertEqual(OTTimecode.decode(timecode: "00:00:00;00"),
+ TCC(d: 0, h: 00, m: 00, s: 00, f: 00, sf: 00))
+
+ XCTAssertEqual(OTTimecode.decode(timecode: "1:56:45;23"),
+ TCC(d: 0, h: 01, m: 56, s: 45, f: 23, sf: 0))
+
+ XCTAssertEqual(OTTimecode.decode(timecode: "01:56:45;23"),
+ TCC(d: 0, h: 01, m: 56, s: 45, f: 23, sf: 0))
+
+ XCTAssertEqual(OTTimecode.decode(timecode: "3 01:56:45;23"),
+ TCC(d: 3, h: 01, m: 56, s: 45, f: 23, sf: 0))
+
+ XCTAssertEqual(OTTimecode.decode(timecode: "12 01:56:45;23"),
+ TCC(d: 12, h: 01, m: 56, s: 45, f: 23, sf: 0))
+
+ XCTAssertEqual(OTTimecode.decode(timecode: "12:01:56:45;23"),
+ TCC(d: 12, h: 01, m: 56, s: 45, f: 23, sf: 0))
+
+
+ // all periods - not supporting this.
+
+ XCTAssertNil(OTTimecode.decode(timecode: "0.0.0.0"))
+ XCTAssertNil(OTTimecode.decode(timecode: "0.00.00.00"))
+ XCTAssertNil(OTTimecode.decode(timecode: "00.00.00.00"))
+ XCTAssertNil(OTTimecode.decode(timecode: "1.56.45.23"))
+ XCTAssertNil(OTTimecode.decode(timecode: "01.56.45.23"))
+ XCTAssertNil(OTTimecode.decode(timecode: "3 01.56.45.23"))
+ XCTAssertNil(OTTimecode.decode(timecode: "12.01.56.45.23"))
+ XCTAssertNil(OTTimecode.decode(timecode: "12.01.56.45.23"))
+
+ // subframes
+
+ XCTAssertEqual(OTTimecode.decode(timecode: "0:00:00:00.05"),
+ TCC(d: 0, h: 00, m: 00, s: 00, f: 00, sf: 05))
+
+ XCTAssertEqual(OTTimecode.decode(timecode: "00:00:00:00.05"),
+ TCC(d: 0, h: 00, m: 00, s: 00, f: 00, sf: 05))
+
+ XCTAssertEqual(OTTimecode.decode(timecode: "1:56:45:23.05"),
+ TCC(d: 0, h: 01, m: 56, s: 45, f: 23, sf: 05))
+
+ XCTAssertEqual(OTTimecode.decode(timecode: "01:56:45:23.05"),
+ TCC(d: 0, h: 01, m: 56, s: 45, f: 23, sf: 05))
+
+ XCTAssertEqual(OTTimecode.decode(timecode: "3 01:56:45:23.05"),
+ TCC(d: 3, h: 01, m: 56, s: 45, f: 23, sf: 05))
+
+ XCTAssertEqual(OTTimecode.decode(timecode: "12 01:56:45:23.05"),
+ TCC(d: 12, h: 01, m: 56, s: 45, f: 23, sf: 05))
+
+ XCTAssertEqual(OTTimecode.decode(timecode: "12:01:56:45:23.05"),
+ TCC(d: 12, h: 01, m: 56, s: 45, f: 23, sf: 05))
+
+
+ }
+
+}
diff --git a/Tests/SwiftTimecode-Unit-Tests/Unit Tests/Math/OTTimecode Math Public Tests.swift b/Tests/SwiftTimecode-Unit-Tests/Unit Tests/Math/OTTimecode Math Public Tests.swift
new file mode 100644
index 00000000..bccae2f7
--- /dev/null
+++ b/Tests/SwiftTimecode-Unit-Tests/Unit Tests/Math/OTTimecode Math Public Tests.swift
@@ -0,0 +1,228 @@
+//
+// OTTimecode Math Public Tests.swift
+// OTTimecodeUnitTests
+//
+// Created by Steffan Andrews on 2020-06-16.
+// Copyright © 2020 Steffan Andrews. All rights reserved.
+//
+
+import XCTest
+@testable import SwiftTimecode
+
+class OTTimecode_UT_Math_Public_Tests: XCTestCase {
+
+ override func setUp() { }
+ override func tearDown() { }
+
+ func testAdd_and_Subtract_Methods() {
+
+ // .add / .subtract methods
+
+ var tc = OTTimecode(at: ._23_976, limit: ._24hours)
+
+ tc = OTTimecode( TCC(h: 00, m: 00, s: 00, f: 00),
+ at: ._23_976, limit: ._24hours)!
+ XCTAssertTrue (tc.add( TCC(h: 00, m: 00, s: 00, f: 23)))
+ XCTAssertEqual(tc.components, TCC(h: 00, m: 00, s: 00, f: 23))
+ XCTAssertTrue (tc.add( TCC(h: 00, m: 00, s: 00, f: 01)))
+ XCTAssertEqual(tc.components, TCC(h: 00, m: 00, s: 01, f: 00))
+
+ tc = OTTimecode( TCC(h: 00, m: 00, s: 00, f: 00),
+ at: ._23_976, limit: ._24hours)!
+ XCTAssertTrue (tc.add( TCC(h: 01, m: 15, s: 30, f: 10)))
+ XCTAssertEqual(tc.components, TCC(h: 01, m: 15, s: 30, f: 10))
+ XCTAssertTrue (tc.add( TCC(h: 01, m: 15, s: 30, f: 10)))
+ XCTAssertEqual(tc.components, TCC(h: 02, m: 31, s: 00, f: 20))
+ XCTAssertFalse(tc.add( TCC(h: 23, m: 15, s: 30, f: 10)))
+ XCTAssertEqual(tc.components, TCC(h: 02, m: 31, s: 00, f: 20)) // unchanged value
+
+ tc = OTTimecode( TCC(h: 00, m: 00, s: 00, f: 00),
+ at: ._23_976, limit: ._24hours)!
+ XCTAssertFalse(tc.subtract( TCC(h: 02, m: 31, s: 00, f: 20)))
+
+ tc = OTTimecode( TCC(h: 23, m: 59, s: 59, f: 23),
+ at: ._23_976, limit: ._24hours)!
+ XCTAssertTrue (tc.subtract( TCC(h: 23, m: 59, s: 59, f: 23)))
+ XCTAssertEqual(tc.components, TCC(h: 00, m: 00, s: 00, f: 00))
+
+ tc = OTTimecode( TCC(h: 23, m: 59, s: 59, f: 23),
+ at: ._23_976, limit: ._24hours)!
+ XCTAssertFalse(tc.subtract( TCC(h: 23, m: 59, s: 59, f: 24))) // 1 frame too many
+ XCTAssertEqual(tc.components, TCC(h: 23, m: 59, s: 59, f: 23)) // unchanged value
+
+ tc = OTTimecode( TCC(h: 00, m: 00, s: 00, f: 00),
+ at: ._23_976, limit: ._24hours)!
+ XCTAssertTrue (tc.add( TCC(f: 24))) // roll up to 1 sec
+ XCTAssertEqual(tc.components, TCC(h: 00, m: 00, s: 01, f: 00))
+
+ tc = OTTimecode( TCC(h: 00, m: 00, s: 00, f: 00),
+ at: ._23_976, limit: ._24hours)!
+ XCTAssertTrue (tc.add( TCC(s: 60))) // roll up to 1 min
+ XCTAssertEqual(tc.components, TCC(h: 00, m: 01, s: 00, f: 00))
+
+ tc = OTTimecode( TCC(h: 00, m: 00, s: 00, f: 00),
+ at: ._23_976, limit: ._24hours)!
+ XCTAssertTrue (tc.add( TCC(m: 60))) // roll up to 1 hr
+ XCTAssertEqual(tc.components, TCC(h: 01, m: 00, s: 00, f: 00))
+
+ tc = OTTimecode( TCC(d: 0, h: 00, m: 00, s: 00, f: 00),
+ at: ._23_976, limit: ._100days)!
+ XCTAssertTrue (tc.add( TCC(h: 24))) // roll up to 1 day
+ XCTAssertEqual(tc.components, TCC(d: 01, h: 00, m: 00, s: 00, f: 00))
+
+ tc = OTTimecode( TCC(h: 00, m: 00, s: 00, f: 00),
+ at: ._23_976, limit: ._24hours)!
+ XCTAssertTrue (tc.add( TCC(h: 00, m: 00, s: 00, f: 2073599)))
+ XCTAssertEqual(tc.components, TCC(h: 23, m: 59, s: 59, f: 23))
+
+ tc = OTTimecode( TCC(h: 23, m: 59, s: 59, f: 23),
+ at: ._23_976, limit: ._24hours)!
+ XCTAssertTrue (tc.subtract( TCC(h: 00, m: 00, s: 00, f: 2073599)))
+ XCTAssertEqual(tc.components, TCC(h: 00, m: 00, s: 00, f: 00))
+
+ XCTAssertTrue (tc.add( TCC(h: 00, m: 00, s: 00, f: 200)))
+ XCTAssertTrue (tc.subtract( TCC(h: 00, m: 00, s: 00, f: 199)))
+ XCTAssertEqual(tc.components, TCC(h: 00, m: 00, s: 00, f: 01))
+
+ // clamping
+
+ tc = OTTimecode( TCC(h: 00, m: 00, s: 00, f: 00),
+ at: ._23_976, limit: ._24hours)!
+ tc.add(clamping: TCC(h: 25))
+ XCTAssertEqual(tc.components, TCC(h: 23, m: 59, s: 59, f: 23, sf: 79))
+
+ tc = OTTimecode( TCC(h: 00, m: 00, s: 00, f: 00),
+ at: ._23_976, limit: ._24hours)!
+ tc.subtract(clamping: TCC(h: 4))
+ XCTAssertEqual(tc.components, TCC(h: 00, m: 00, s: 00, f: 00))
+
+ // wrapping
+
+ tc = OTTimecode( TCC(h: 00, m: 00, s: 00, f: 00),
+ at: ._23_976, limit: ._24hours)!
+ tc.add(wrapping: TCC(h: 25))
+ XCTAssertEqual(tc.components, TCC(h: 01, m: 00, s: 00, f: 00))
+
+ tc = OTTimecode( TCC(h: 00, m: 00, s: 00, f: 00),
+ at: ._23_976, limit: ._24hours)!
+ tc.add(wrapping: TCC(f: -1)) // add negative number
+ XCTAssertEqual(tc.components, TCC(h: 23, m: 59, s: 59, f: 23))
+
+ tc = OTTimecode( TCC(h: 00, m: 00, s: 00, f: 00),
+ at: ._23_976, limit: ._24hours)!
+ tc.subtract(wrapping: TCC(h: 4))
+ XCTAssertEqual(tc.components, TCC(h: 20, m: 00, s: 00, f: 00))
+
+ tc = OTTimecode( TCC(h: 00, m: 00, s: 00, f: 00),
+ at: ._23_976, limit: ._24hours)!
+ tc.subtract(wrapping: TCC(h: -4)) // subtract negative number
+ XCTAssertEqual(tc.components, TCC(h: 04, m: 00, s: 00, f: 00))
+
+ // drop-frame frame rates
+
+ tc = OTTimecode( TCC(h: 00, m: 00, s: 00, f: 00),
+ at: ._29_97_drop, limit: ._24hours)!
+ XCTAssertTrue (tc.add( TCC(h: 00, m: 00, s: 00, f: 29)))
+ XCTAssertEqual(tc.components, TCC(h: 00, m: 00, s: 00, f: 29))
+ XCTAssertTrue (tc.add( TCC(h: 00, m: 00, s: 00, f: 01)))
+ XCTAssertEqual(tc.components, TCC(h: 00, m: 00, s: 01, f: 00))
+
+ tc = OTTimecode( TCC(h: 00, m: 00, s: 00, f: 00),
+ at: ._29_97_drop, limit: ._24hours)!
+ XCTAssertTrue (tc.add( TCC(m: 60))) // roll up to 1 hr
+ XCTAssertEqual(tc.components, TCC(h: 01, m: 00, s: 00, f: 00))
+
+ tc = OTTimecode( TCC(h: 00, m: 00, s: 00, f: 00),
+ at: ._29_97_drop, limit: ._24hours)!
+ XCTAssertTrue (tc.add( TCC(f: 30))) // roll up to 1 sec
+ XCTAssertEqual(tc.components, TCC(h: 00, m: 00, s: 01, f: 00))
+
+ tc = OTTimecode( TCC(h: 00, m: 00, s: 59, f: 00),
+ at: ._29_97_drop, limit: ._24hours)!
+ XCTAssertTrue (tc.add( TCC(f: 30))) // roll up to 1 sec and 2 frames (2 dropped frames every minute except every 10th minute)
+ XCTAssertEqual(tc.components, TCC(h: 00, m: 01, s: 00, f: 02))
+
+ tc = OTTimecode( TCC(h: 00, m: 01, s: 00, f: 02),
+ at: ._29_97_drop, limit: ._24hours)!
+ XCTAssertTrue (tc.add( TCC(m: 01))) // roll up to 1 sec and 2 frames (2 dropped frames every minute except every 10th minute)
+ XCTAssertEqual(tc.components, TCC(h: 00, m: 02, s: 00, f: 02))
+ XCTAssertTrue (tc.add( TCC(m: 08)))
+ XCTAssertEqual(tc.components, TCC(h: 00, m: 10, s: 00, f: 00))
+
+ }
+
+ func testMultiply_and_Divide() {
+
+ // .multiply / .divide methods
+
+ var tc = OTTimecode( TCC(h: 01, m: 00, s: 00, f: 00),
+ at: ._23_976, limit: ._24hours)!
+ XCTAssertTrue (tc.multiply(2))
+ XCTAssertEqual(tc.components, TCC(h: 02, m: 00, s: 00, f: 00))
+ tc = OTTimecode( TCC(h: 01, m: 00, s: 00, f: 00),
+ at: ._23_976, limit: ._24hours)!
+ XCTAssertTrue (tc.multiply(2.5))
+ XCTAssertEqual(tc.components, TCC(h: 02, m: 30, s: 00, f: 00))
+
+ tc = OTTimecode( TCC(h: 01, m: 00, s: 00, f: 00),
+ at: ._29_97_drop, limit: ._24hours)!
+ XCTAssertTrue (tc.multiply(2))
+ XCTAssertEqual(tc.components, TCC(h: 02, m: 00, s: 00, f: 00))
+ tc = OTTimecode( TCC(h: 01, m: 00, s: 00, f: 00),
+ at: ._29_97_drop, limit: ._24hours)!
+ XCTAssertTrue (tc.multiply(2.5))
+ XCTAssertEqual(tc.components, TCC(h: 02, m: 30, s: 00, f: 00))
+ tc = OTTimecode( TCC(h: 01, m: 00, s: 00, f: 00),
+ at: ._29_97_drop, limit: ._24hours)!
+ XCTAssertFalse(tc.multiply(25))
+ XCTAssertEqual(tc.components, TCC(h: 01, m: 00, s: 00, f: 00)) // unchanged
+
+ // clamping
+
+ tc = OTTimecode( TCC(h: 01, m: 00, s: 00, f: 00),
+ at: ._23_976, limit: ._24hours)!
+ tc.multiply(clamping: 25.0)
+ XCTAssertEqual(tc.components, TCC(h: 23, m: 59, s: 59, f: 23))
+
+ tc = OTTimecode( TCC(h: 00, m: 00, s: 00, f: 00),
+ at: ._23_976, limit: ._24hours)!
+ tc.divide(clamping: 4)
+ XCTAssertEqual(tc.components, TCC(h: 00, m: 00, s: 00, f: 00))
+
+ // wrapping - multiply
+
+ tc = OTTimecode( TCC(h: 01, m: 00, s: 00, f: 00),
+ at: ._23_976, limit: ._24hours)!
+ tc.multiply(wrapping: 25.0)
+ XCTAssertEqual(tc.components, TCC(h: 01, m: 00, s: 00, f: 00))
+
+ tc = OTTimecode( TCC(h: 01, m: 00, s: 00, f: 00),
+ at: ._23_976, limit: ._24hours)!
+ tc.multiply(wrapping: 2)
+ XCTAssertEqual(tc.components, TCC(h: 02, m: 00, s: 00, f: 00)) // normal, no wrap
+
+ tc = OTTimecode( TCC(h: 01, m: 00, s: 00, f: 00),
+ at: ._23_976, limit: ._24hours)!
+ tc.multiply(wrapping: 25)
+ XCTAssertEqual(tc.components, TCC(h: 01, m: 00, s: 00, f: 00)) // wraps
+
+ // wrapping - divide
+
+ tc = OTTimecode( TCC(h: 01, m: 00, s: 00, f: 00),
+ at: ._23_976, limit: ._24hours)!
+ tc.divide(wrapping: -2)
+ XCTAssertEqual(tc.components, TCC(h: 23, m: 30, s: 00, f: 00))
+
+ tc = OTTimecode( TCC(h: 01, m: 00, s: 00, f: 00),
+ at: ._23_976, limit: ._24hours)!
+ tc.divide(wrapping: 2)
+ XCTAssertEqual(tc.components, TCC(h: 00, m: 30, s: 00, f: 00)) // normal, no wrap
+
+ tc = OTTimecode( TCC(h: 12, m: 00, s: 00, f: 00),
+ at: ._23_976, limit: ._24hours)!
+ tc.divide(wrapping: -2)
+ XCTAssertEqual(tc.components, TCC(h: 18, m: 00, s: 00, f: 00)) // wraps
+
+ }
+
+}
diff --git a/Tests/SwiftTimecode-Unit-Tests/Unit Tests/Math/OTTimecode Operators Tests.swift b/Tests/SwiftTimecode-Unit-Tests/Unit Tests/Math/OTTimecode Operators Tests.swift
new file mode 100644
index 00000000..765b6d18
--- /dev/null
+++ b/Tests/SwiftTimecode-Unit-Tests/Unit Tests/Math/OTTimecode Operators Tests.swift
@@ -0,0 +1,69 @@
+//
+// OTTimecode Operators Tests.swift
+// OTTimecodeUnitTests
+//
+// Created by Steffan Andrews on 2020-06-16.
+// Copyright © 2020 Steffan Andrews. All rights reserved.
+//
+
+import XCTest
+@testable import SwiftTimecode
+
+class OTTimecode_UT_Operators_Tests: XCTestCase {
+
+ override func setUp() { }
+ override func tearDown() { }
+
+ func testAdd_and_Subtract_Operators() {
+
+ var tc = OTTimecode(at: ._30)
+
+ // + and - operators
+
+ tc = TCC(h: 00, m: 00, s: 00, f: 00).toTimecode(at: ._30)!
+
+ tc = tc + TCC(h: 00, m: 00, s: 00, f: 05).toTimecode(at: ._30)!
+ XCTAssertEqual(tc.components, TCC(h: 00, m: 00, s: 00, f: 05))
+
+ tc = tc - TCC(h: 00, m: 00, s: 00, f: 04).toTimecode(at: ._30)!
+ XCTAssertEqual(tc.components, TCC(h: 00, m: 00, s: 00, f: 01))
+
+ // += and -= operators
+
+ tc = TCC(h: 00, m: 00, s: 00, f: 00).toTimecode(at: ._30)!
+
+ tc += TCC(h: 00, m: 00, s: 00, f: 05).toTimecode(at: ._30)!
+ XCTAssertEqual(tc.components, TCC(h: 00, m: 00, s: 00, f: 05))
+
+ tc -= TCC(h: 00, m: 00, s: 00, f: 04).toTimecode(at: ._30)!
+ XCTAssertEqual(tc.components, TCC(h: 00, m: 00, s: 00, f: 01))
+
+ }
+
+ func testMultiply_and_Divide_Operators() {
+
+ var tc = OTTimecode(at: ._30)
+
+ // * and / operators
+
+ tc = TCC(h: 01, m: 00, s: 00, f: 00).toTimecode(at: ._30)!
+
+ tc = tc * 5
+ XCTAssertEqual(tc.components, TCC(h: 05, m: 00, s: 00, f: 00))
+
+ tc = tc / 5
+ XCTAssertEqual(tc.components, TCC(h: 01, m: 00, s: 00, f: 00))
+
+ // += and -= operators
+
+ tc = TCC(h: 01, m: 00, s: 00, f: 00).toTimecode(at: ._30)!
+
+ tc *= 5
+ XCTAssertEqual(tc.components, TCC(h: 05, m: 00, s: 00, f: 00))
+
+ tc /= 5
+ XCTAssertEqual(tc.components, TCC(h: 01, m: 00, s: 00, f: 00))
+
+ }
+
+}
diff --git a/Tests/SwiftTimecode-Unit-Tests/Unit Tests/OTTime/OTTime Tests.swift b/Tests/SwiftTimecode-Unit-Tests/Unit Tests/OTTime/OTTime Tests.swift
new file mode 100644
index 00000000..59959e59
--- /dev/null
+++ b/Tests/SwiftTimecode-Unit-Tests/Unit Tests/OTTime/OTTime Tests.swift
@@ -0,0 +1,65 @@
+//
+// OTTime Tests.swift
+// OTTimecodeUnitTests
+//
+// Created by Steffan Andrews on 2020-06-18.
+// Copyright © 2020 Steffan Andrews. All rights reserved.
+//
+
+import XCTest
+@testable import SwiftTimecode
+
+class OTTime_UT_Tests: XCTestCase {
+
+ override func setUp() { }
+ override func tearDown() { }
+
+ func testMS() {
+
+ let time = 50505050.123456789123456789
+
+ let ott = OTTime(ms: time)
+
+ XCTAssertEqual(ott.backing, .ms)
+ XCTAssertEqual(ott.ms, time)
+ XCTAssertEqual(ott.seconds, time / 1000.0)
+
+ }
+
+ func testSeconds() {
+
+ let time = 50505050.123456789123456789
+
+ let ott = OTTime(seconds: time)
+
+ XCTAssertEqual(ott.backing, .seconds)
+ XCTAssertEqual(ott.ms, time * 1000.0)
+ XCTAssertEqual(ott.seconds, time)
+
+ }
+
+ func testEquatable() {
+
+ let ott1 = OTTime(seconds: 5.5)
+ let ott2 = OTTime(seconds: 5.5)
+
+ XCTAssertEqual(ott1, ott2)
+
+ let ott3 = OTTime(seconds: 5.5)
+ let ott4 = OTTime(ms: 5500)
+
+ XCTAssertEqual(ott3, ott4)
+
+ }
+
+ func testComparable() {
+
+ let ott1 = OTTime(seconds: 5.5)
+ let ott2 = OTTime(seconds: 6.0)
+
+ XCTAssertTrue(ott1 < ott2)
+ XCTAssertTrue(ott2 > ott1)
+
+ }
+
+}
diff --git a/Tests/SwiftTimecode-Unit-Tests/Unit Tests/OTTimecode Elapsed Frames.swift b/Tests/SwiftTimecode-Unit-Tests/Unit Tests/OTTimecode Elapsed Frames.swift
new file mode 100644
index 00000000..6d8a1ae0
--- /dev/null
+++ b/Tests/SwiftTimecode-Unit-Tests/Unit Tests/OTTimecode Elapsed Frames.swift
@@ -0,0 +1,95 @@
+//
+// OTTimecode Elapsed Frames.swift
+// SwiftTimecodeTests
+//
+// Created by Steffan Andrews on 2020-06-16.
+// Copyright © 2020 Steffan Andrews. All rights reserved.
+//
+
+import XCTest
+@testable import SwiftTimecode
+
+class OTTimecode_UT_Elapsed_Frames: XCTestCase {
+
+ override func setUp() { }
+ override func tearDown() { }
+
+ func testAllFrameRates_ElapsedFrames() {
+
+ // duration of 24 hours elapsed, rolling over to 1 day
+
+ // also helps ensure Strideable .distance(to:) returns the correct values
+
+ OTTimecode.FrameRate.allCases.forEach {
+
+ // max frames in 24 hours
+
+ var maxFramesIn24hours = 0
+ switch $0 {
+ case ._23_976 : maxFramesIn24hours = 2073600
+ case ._24 : maxFramesIn24hours = 2073600
+ case ._24_98 : maxFramesIn24hours = 2160000
+ case ._25 : maxFramesIn24hours = 2160000
+ case ._29_97 : maxFramesIn24hours = 2592000
+ case ._29_97_drop : maxFramesIn24hours = 2589408
+ case ._30 : maxFramesIn24hours = 2592000
+ case ._30_drop : maxFramesIn24hours = 2589408
+ case ._47_952 : maxFramesIn24hours = 4147200
+ case ._48 : maxFramesIn24hours = 4147200
+ case ._50 : maxFramesIn24hours = 4320000
+ case ._59_94 : maxFramesIn24hours = 5184000
+ case ._59_94_drop : maxFramesIn24hours = 5178816
+ case ._60 : maxFramesIn24hours = 5184000
+ case ._60_drop : maxFramesIn24hours = 5178816
+ case ._100 : maxFramesIn24hours = 8640000
+ case ._119_88 : maxFramesIn24hours = 10368000
+ case ._119_88_drop : maxFramesIn24hours = 10357632
+ case ._120 : maxFramesIn24hours = 10368000
+ case ._120_drop : maxFramesIn24hours = 10357632
+ }
+
+ XCTAssertEqual($0.maxTotalFrames(in: ._24hours),
+ maxFramesIn24hours,
+ "for \($0)")
+
+ }
+
+ // number of total elapsed frames in (24 hours - 1 frame), or essentially the maximum timecode expressable for each frame rate
+
+ OTTimecode.FrameRate.allCases.forEach {
+
+ // max frames in 24 hours - 1
+
+ var maxFramesExpressibleIn24hours = 0
+ switch $0 {
+ case ._23_976 : maxFramesExpressibleIn24hours = 2073600 - 1
+ case ._24 : maxFramesExpressibleIn24hours = 2073600 - 1
+ case ._24_98 : maxFramesExpressibleIn24hours = 2160000 - 1
+ case ._25 : maxFramesExpressibleIn24hours = 2160000 - 1
+ case ._29_97 : maxFramesExpressibleIn24hours = 2592000 - 1
+ case ._29_97_drop : maxFramesExpressibleIn24hours = 2589408 - 1
+ case ._30 : maxFramesExpressibleIn24hours = 2592000 - 1
+ case ._30_drop : maxFramesExpressibleIn24hours = 2589408 - 1
+ case ._47_952 : maxFramesExpressibleIn24hours = 4147200 - 1
+ case ._48 : maxFramesExpressibleIn24hours = 4147200 - 1
+ case ._50 : maxFramesExpressibleIn24hours = 4320000 - 1
+ case ._59_94 : maxFramesExpressibleIn24hours = 5184000 - 1
+ case ._59_94_drop : maxFramesExpressibleIn24hours = 5178816 - 1
+ case ._60 : maxFramesExpressibleIn24hours = 5184000 - 1
+ case ._60_drop : maxFramesExpressibleIn24hours = 5178816 - 1
+ case ._100 : maxFramesExpressibleIn24hours = 8640000 - 1
+ case ._119_88 : maxFramesExpressibleIn24hours = 10368000 - 1
+ case ._119_88_drop : maxFramesExpressibleIn24hours = 10357632 - 1
+ case ._120 : maxFramesExpressibleIn24hours = 10368000 - 1
+ case ._120_drop : maxFramesExpressibleIn24hours = 10357632 - 1
+ }
+
+ XCTAssertEqual($0.maxTotalFramesExpressible(in: ._24hours),
+ maxFramesExpressibleIn24hours,
+ "for \($0)")
+
+ }
+
+ }
+
+}
diff --git a/Tests/SwiftTimecode-Unit-Tests/Unit Tests/OTTimecode String Extensions Tests.swift b/Tests/SwiftTimecode-Unit-Tests/Unit Tests/OTTimecode String Extensions Tests.swift
new file mode 100644
index 00000000..78c4df4b
--- /dev/null
+++ b/Tests/SwiftTimecode-Unit-Tests/Unit Tests/OTTimecode String Extensions Tests.swift
@@ -0,0 +1,25 @@
+//
+// OTTimecode String Extensions Tests.swift
+// SwiftTimecodeTests
+//
+// Created by Steffan Andrews on 2020-06-16.
+// Copyright © 2020 Steffan Andrews. All rights reserved.
+//
+
+import XCTest
+@testable import SwiftTimecode
+
+class OTTimecode_UT_Swift_Extensions_Tests: XCTestCase {
+
+ override func setUp() { }
+ override func tearDown() { }
+
+
+ func testString_toTimeCode() {
+
+ XCTAssertEqual("01:05:20:14".toTimecode(at: ._23_976),
+ OTTimecode(TCC(h: 1, m: 5, s: 20, f: 14), at: ._23_976))
+
+ }
+
+}
diff --git a/Tests/SwiftTimecode-Unit-Tests/Unit Tests/OTTimecode Tests.swift b/Tests/SwiftTimecode-Unit-Tests/Unit Tests/OTTimecode Tests.swift
new file mode 100644
index 00000000..b00e269b
--- /dev/null
+++ b/Tests/SwiftTimecode-Unit-Tests/Unit Tests/OTTimecode Tests.swift
@@ -0,0 +1,31 @@
+//
+// OTTimecode Tests.swift
+// SwiftTimecodeTests
+//
+// Created by Steffan Andrews on 2020-06-16.
+// Copyright © 2020 Steffan Andrews. All rights reserved.
+//
+
+import XCTest
+@testable import SwiftTimecode
+
+class OTTimecode_UT_Tests: XCTestCase {
+
+ override func setUp() { }
+ override func tearDown() { }
+
+ func testCustomStringConvertible() {
+
+ let tc = OTTimecode(TCC(d: 1, h: 2, m: 3, s: 4, f: 5, sf: 6),
+ at: ._24,
+ limit: ._100days)!
+
+ XCTAssertNotEqual(tc.description, "")
+
+ XCTAssertNotEqual(tc.debugDescription, "")
+
+ XCTAssertNotEqual(tc.verboseDescription, "")
+
+ }
+
+}
diff --git a/Tests/SwiftTimecode-Unit-Tests/Unit Tests/OTTimecode Validation Tests.swift b/Tests/SwiftTimecode-Unit-Tests/Unit Tests/OTTimecode Validation Tests.swift
new file mode 100644
index 00000000..bf68c1f4
--- /dev/null
+++ b/Tests/SwiftTimecode-Unit-Tests/Unit Tests/OTTimecode Validation Tests.swift
@@ -0,0 +1,150 @@
+//
+// OTTimecode Validation Tests.swift
+// SwiftTimecodeTests
+//
+// Created by Steffan Andrews on 2020-06-16.
+// Copyright © 2020 Steffan Andrews. All rights reserved.
+//
+
+import XCTest
+@testable import SwiftTimecode
+
+class OTTimecode_UT_Validation_Tests: XCTestCase {
+
+ override func setUp() { }
+ override func tearDown() { }
+
+ func testValidWithinRanges() {
+
+ // typical valid values
+
+ let fr = OTTimecode.FrameRate._24
+ let limit = OTTimecode.UpperLimit._24hours
+
+ let tc = OTTimecode(at: fr, limit: limit)
+
+ XCTAssertEqual(tc.invalidComponents, [])
+ XCTAssertEqual(tc.components.invalidComponents(at: fr, limit: limit, subFramesDivisor: 80), [])
+
+ XCTAssertEqual(tc.validRange(of: .days), 0...0)
+ XCTAssertEqual(tc.validRange(of: .hours), 0...23)
+ XCTAssertEqual(tc.validRange(of: .minutes), 0...59)
+ XCTAssertEqual(tc.validRange(of: .seconds), 0...59)
+ XCTAssertEqual(tc.validRange(of: .frames), 0...23)
+ //XCTAssertThrowsError(tc.validRange(of: .subFrames)) // *****
+
+ }
+
+ func testInvalidOverRanges() {
+
+ // invalid - over ranges
+
+ let fr = OTTimecode.FrameRate._24
+ let limit = OTTimecode.UpperLimit._24hours
+
+ var tc = OTTimecode(at: fr, limit: limit)
+ tc.days = 5
+ tc.hours = 25
+ tc.minutes = 75
+ tc.seconds = 75
+ tc.frames = 52
+ tc.subFrames = 500
+
+ XCTAssertEqual(tc.invalidComponents,
+ [.days, .hours, .minutes, .seconds, .frames, .subFrames])
+ XCTAssertEqual(tc.components.invalidComponents(at: fr, limit: limit, subFramesDivisor: 80),
+ [.days, .hours, .minutes, .seconds, .frames, .subFrames])
+
+ }
+
+ func testInvalidUnderRanges() {
+
+ // invalid - under ranges
+
+ let fr = OTTimecode.FrameRate._24
+ let limit = OTTimecode.UpperLimit._24hours
+
+ var tc = OTTimecode(at: fr, limit: limit)
+ tc.days = -1
+ tc.hours = -1
+ tc.minutes = -1
+ tc.seconds = -1
+ tc.frames = -1
+ tc.subFrames = -1
+
+ XCTAssertEqual(tc.invalidComponents,
+ [.days, .hours, .minutes, .seconds, .frames, .subFrames])
+ XCTAssertEqual(tc.components.invalidComponents(at: fr, limit: limit, subFramesDivisor: 80),
+ [.days, .hours, .minutes, .seconds, .frames, .subFrames])
+
+ }
+
+ func testDropFrame() {
+
+ // perform a spot-check to ensure drop-frame timecode validation works as expected
+
+ OTTimecode.FrameRate.allDrop.forEach {
+
+ let limit = OTTimecode.UpperLimit._24hours
+
+ // every 10 minutes, no frames are skipped
+
+ do {
+ var tc = OTTimecode(at: $0, limit: limit)
+ tc.minutes = 0
+ tc.frames = 0
+
+ XCTAssertEqual(tc.invalidComponents,
+ [], "for \($0)")
+ XCTAssertEqual(tc.components.invalidComponents(at: $0, limit: limit, subFramesDivisor: 80),
+ [], "for \($0)")
+ }
+
+ // all other minutes each skip frame 0 and 1
+
+ for minute in 1...9 {
+ var tc = OTTimecode(at: $0, limit: limit)
+ tc.minutes = minute
+ tc.frames = 0
+
+ XCTAssertEqual(tc.invalidComponents,
+ [.frames], "for \($0) at \(minute) minutes")
+ XCTAssertEqual(tc.components.invalidComponents(at: $0, limit: limit, subFramesDivisor: 80),
+ [.frames], "for \($0) at \(minute) minutes")
+
+ tc = OTTimecode(at: $0, limit: limit)
+ tc.minutes = minute
+ tc.frames = 1
+
+ XCTAssertEqual(tc.invalidComponents,
+ [.frames], "for \($0) at \(minute) minutes")
+ XCTAssertEqual(tc.components.invalidComponents(at: $0, limit: limit, subFramesDivisor: 80),
+ [.frames], "for \($0) at \(minute) minutes")
+ }
+
+ }
+
+ }
+
+ func testMaxFrames() {
+
+ let tc = OTTimecode(at: ._24, limit: ._24hours)
+
+ let mf = tc.maxFramesAndSubframesExpressible
+
+ let tcc = OTTimecode.components(from: mf,
+ at: tc.frameRate,
+ subFramesDivisor: tc.subFramesDivisor)
+
+ XCTAssertEqual(tc.validRange(of: .subFrames), 0...79)
+ XCTAssertEqual(mf, 2073599.9875)
+ XCTAssertEqual(tc.subFrames, 0)
+ XCTAssertEqual(tc.subFramesDivisor, 80)
+
+ XCTAssertEqual(tcc, TCC(d: 0, h: 23, m: 59, s: 59, f: 23, sf: 79))
+
+ }
+
+}
+
+
diff --git a/Tests/SwiftTimecode-Unit-Tests/Unit Tests/OTTimecode init Tests.swift b/Tests/SwiftTimecode-Unit-Tests/Unit Tests/OTTimecode init Tests.swift
new file mode 100644
index 00000000..4b105cb3
--- /dev/null
+++ b/Tests/SwiftTimecode-Unit-Tests/Unit Tests/OTTimecode init Tests.swift
@@ -0,0 +1,95 @@
+//
+// OTTimecode init Tests.swift
+// SwiftTimecodeTests
+//
+// Created by Steffan Andrews on 2020-06-16.
+// Copyright © 2020 Steffan Andrews. All rights reserved.
+//
+
+import XCTest
+@testable import SwiftTimecode
+
+class OTTimecode_UT_init_Tests: XCTestCase {
+
+ override func setUp() { }
+ override func tearDown() { }
+
+
+ func testOTTimecode_init_Defaults() {
+ // essential inits
+
+ // defaults
+
+ var tc = OTTimecode(at: ._24)
+ XCTAssertEqual(tc.frameRate, ._24)
+ XCTAssertEqual(tc.upperLimit, ._24hours)
+ XCTAssertEqual(tc.totalElapsedFrames, 0)
+ XCTAssertEqual(tc.components, TCC(d: 0, h: 0, m: 0, s: 0, f: 0))
+ XCTAssertEqual(tc.stringValue, "00:00:00:00")
+
+ // expected intitalizers
+
+ tc = OTTimecode(at: ._24)
+ tc = OTTimecode(at: ._24, limit: ._24hours)
+
+ }
+
+ // ____ basic inits, using (exactly: ) ____
+
+ func testOTTimecode_init_String() {
+
+ OTTimecode.FrameRate.allCases.forEach {
+ let tc = OTTimecode("00:00:00:00", at: $0, limit: ._24hours)
+
+ XCTAssertEqual(tc?.days , 0 , "for \($0)")
+ XCTAssertEqual(tc?.hours , 0 , "for \($0)")
+ XCTAssertEqual(tc?.minutes , 0 , "for \($0)")
+ XCTAssertEqual(tc?.seconds , 0 , "for \($0)")
+ XCTAssertEqual(tc?.frames , 0 , "for \($0)")
+ XCTAssertEqual(tc?.subFrames, 0 , "for \($0)")
+ }
+
+ OTTimecode.FrameRate.allCases.forEach {
+ let tc = OTTimecode("01:02:03:04", at: $0, limit: ._24hours)
+
+ XCTAssertEqual(tc?.days , 0 , "for \($0)")
+ XCTAssertEqual(tc?.hours , 1 , "for \($0)")
+ XCTAssertEqual(tc?.minutes , 2 , "for \($0)")
+ XCTAssertEqual(tc?.seconds , 3 , "for \($0)")
+ XCTAssertEqual(tc?.frames , 4 , "for \($0)")
+ XCTAssertEqual(tc?.subFrames, 0 , "for \($0)")
+ }
+
+ }
+
+ func testOTTimecode_init_Components() {
+
+ OTTimecode.FrameRate.allCases.forEach {
+ let tc = OTTimecode(TCC(d: 0, h: 0, m: 0, s: 0, f: 0),
+ at: $0,
+ limit: ._24hours)
+
+ XCTAssertEqual(tc?.days , 0 , "for \($0)")
+ XCTAssertEqual(tc?.hours , 0 , "for \($0)")
+ XCTAssertEqual(tc?.minutes , 0 , "for \($0)")
+ XCTAssertEqual(tc?.seconds , 0 , "for \($0)")
+ XCTAssertEqual(tc?.frames , 0 , "for \($0)")
+ XCTAssertEqual(tc?.subFrames, 0 , "for \($0)")
+ }
+
+ OTTimecode.FrameRate.allCases.forEach {
+ let tc = OTTimecode(TCC(d: 0, h: 1, m: 2, s: 3, f: 4),
+ at: $0,
+ limit: ._24hours)
+
+ XCTAssertEqual(tc?.days , 0 , "for \($0)")
+ XCTAssertEqual(tc?.hours , 1 , "for \($0)")
+ XCTAssertEqual(tc?.minutes , 2 , "for \($0)")
+ XCTAssertEqual(tc?.seconds , 3 , "for \($0)")
+ XCTAssertEqual(tc?.frames , 4 , "for \($0)")
+ XCTAssertEqual(tc?.subFrames, 0 , "for \($0)")
+ }
+
+ }
+
+}
diff --git a/Tests/SwiftTimecode-Unit-Tests/Unit Tests/Protocol Adoptions/Comparable Tests.swift b/Tests/SwiftTimecode-Unit-Tests/Unit Tests/Protocol Adoptions/Comparable Tests.swift
new file mode 100644
index 00000000..70595fcb
--- /dev/null
+++ b/Tests/SwiftTimecode-Unit-Tests/Unit Tests/Protocol Adoptions/Comparable Tests.swift
@@ -0,0 +1,37 @@
+//
+// Comparable Tests.swift
+// OTTimecodeUnitTests
+//
+// Created by Steffan Andrews on 2020-06-16.
+// Copyright © 2020 Steffan Andrews. All rights reserved.
+//
+
+import XCTest
+@testable import SwiftTimecode
+
+class OTTimecode_UT_Comparable_Tests: XCTestCase {
+
+ override func setUp() { }
+ override func tearDown() { }
+
+ func testOTTimecode_Equatable_Comparable() {
+
+ // ==
+
+ XCTAssertEqual( "01:00:00:00".toTimecode(at: ._23_976)!, "01:00:00:00".toTimecode(at: ._23_976)!)
+ XCTAssertEqual( "01:00:00:00".toTimecode(at: ._23_976)!, "01:00:00:00".toTimecode(at: ._29_97)!)
+
+ // == where elapsed frame count matches but frame rate differs (two frame rates where elapsed frames in 24 hours is identical)
+
+ XCTAssertNotEqual("01:00:00:00".toTimecode(at: ._23_976)!, "01:00:00:00".toTimecode(at: ._24)!)
+
+ // < >
+
+ XCTAssertFalse( "01:00:00:00".toTimecode(at: ._23_976)! < "01:00:00:00".toTimecode(at: ._29_97)!) // false because they're ==
+ XCTAssertFalse( "01:00:00:00".toTimecode(at: ._23_976)! > "01:00:00:00".toTimecode(at: ._29_97)!) // false because they're ==
+
+ XCTAssertFalse( "01:00:00:00".toTimecode(at: ._23_976)! < "01:00:00:00".toTimecode(at: ._23_976)!) // false because they're ==
+ XCTAssertFalse( "01:00:00:00".toTimecode(at: ._23_976)! > "01:00:00:00".toTimecode(at: ._23_976)!) // false because they're ==
+ }
+
+}
diff --git a/Tests/SwiftTimecode-Unit-Tests/Unit Tests/Protocol Adoptions/Hashable Tests.swift b/Tests/SwiftTimecode-Unit-Tests/Unit Tests/Protocol Adoptions/Hashable Tests.swift
new file mode 100644
index 00000000..bcc54b76
--- /dev/null
+++ b/Tests/SwiftTimecode-Unit-Tests/Unit Tests/Protocol Adoptions/Hashable Tests.swift
@@ -0,0 +1,67 @@
+//
+// Hashable Tests.swift
+// OTTimecodeUnitTests
+//
+// Created by Steffan Andrews on 2020-06-16.
+// Copyright © 2020 Steffan Andrews. All rights reserved.
+//
+
+import XCTest
+@testable import SwiftTimecode
+
+class OTTimecode_UT_Hashable_Tests: XCTestCase {
+
+ override func setUp() { }
+ override func tearDown() { }
+
+ func testHashValue() {
+
+ // hashValues should be equal
+
+ XCTAssertEqual( "01:00:00:00".toTimecode(at: ._23_976)!.hashValue,
+ "01:00:00:00".toTimecode(at: ._23_976)!.hashValue)
+ XCTAssertNotEqual("01:00:00:01".toTimecode(at: ._23_976)!.hashValue,
+ "01:00:00:00".toTimecode(at: ._23_976)!.hashValue)
+
+ XCTAssertNotEqual("01:00:00:00".toTimecode(at: ._23_976)!.hashValue,
+ "01:00:00:00".toTimecode(at: ._24)!.hashValue)
+ XCTAssertNotEqual("01:00:00:00".toTimecode(at: ._23_976)!.hashValue,
+ "01:00:00:00".toTimecode(at: ._29_97)!.hashValue)
+
+ }
+
+ func testDictionary() {
+
+ // Dictionary / Set
+
+ var dict: [OTTimecode : String] = [:]
+ dict["01:00:00:00".toTimecode(at: ._23_976)!] = "A Spot Note Here"
+ dict["01:00:00:06".toTimecode(at: ._23_976)!] = "A Spot Note Also Here"
+ XCTAssertEqual(dict.count, 2)
+ dict["01:00:00:00".toTimecode(at: ._24)!] = "This should not replace"
+ XCTAssertEqual(dict.count, 3)
+
+ XCTAssertEqual(dict["01:00:00:00".toTimecode(at: ._23_976)!], "A Spot Note Here")
+ XCTAssertEqual(dict["01:00:00:00".toTimecode(at: ._24)!], "This should not replace")
+
+ }
+
+ func testSet() {
+
+ // unique timecodes are based on frame counts, irrespective of frame rate
+
+ let tcSet: Set = ["01:00:00:00".toTimecode(at: ._23_976)!,
+ "01:00:00:00".toTimecode(at: ._24)!,
+ "01:00:00:00".toTimecode(at: ._25)!,
+ "01:00:00:00".toTimecode(at: ._29_97)!,
+ "01:00:00:00".toTimecode(at: ._29_97_drop)!,
+ "01:00:00:00".toTimecode(at: ._30)!,
+ "01:00:00:00".toTimecode(at: ._59_94)!,
+ "01:00:00:00".toTimecode(at: ._59_94_drop)!,
+ "01:00:00:00".toTimecode(at: ._60)!]
+
+ XCTAssertNotEqual(tcSet.count, 1) // doesn't matter what frame rate it is, the same total elapsed frames matters
+
+ }
+
+}
diff --git a/Tests/SwiftTimecode-Unit-Tests/Unit Tests/Protocol Adoptions/Strideable Tests.swift b/Tests/SwiftTimecode-Unit-Tests/Unit Tests/Protocol Adoptions/Strideable Tests.swift
new file mode 100644
index 00000000..25f07a91
--- /dev/null
+++ b/Tests/SwiftTimecode-Unit-Tests/Unit Tests/Protocol Adoptions/Strideable Tests.swift
@@ -0,0 +1,137 @@
+//
+// Strideable Tests.swift
+// SwiftTimecodeTests
+//
+// Created by Steffan Andrews on 2020-06-16.
+// Copyright © 2020 Steffan Andrews. All rights reserved.
+//
+
+import XCTest
+@testable import SwiftTimecode
+
+class OTTimecode_UT_Strideable_Tests: XCTestCase {
+
+ override func setUp() { }
+ override func tearDown() { }
+
+ func testAdvancedBy() {
+
+ OTTimecode.FrameRate.allCases.forEach {
+
+ let frames = Int(OTTimecode.totalElapsedFrames(of: TCC(h: 1), at: $0))
+
+ let advanced = TCC(f: 00)
+ .toTimecode(at: $0)!
+ .advanced(by: frames)
+ .components
+
+ XCTAssertEqual(advanced, TCC(h: 1), "for \($0)")
+
+ }
+ }
+
+ func testDistanceTo_24Hours() {
+
+ // 24 hours stride frame count test
+
+ OTTimecode.FrameRate.allCases.forEach {
+
+ let zero = TCC(h: 00, m: 00, s: 00, f: 00)
+ .toTimecode(at: $0)!
+
+ let target = TCC(d: 00, h: 23, m: 59, s: 59, f: $0.maxFrameNumberDisplayable)
+ .toTimecode(at: $0)!
+
+ let delta = zero.distance(to: target)
+
+ XCTAssertEqual(delta, $0.maxTotalFramesExpressible(in: ._24hours), "for \($0)")
+
+ }
+
+ }
+
+ func testDistanceTo_100Days() {
+
+ // 100 days stride frame count test
+
+ OTTimecode.FrameRate.allCases.forEach {
+
+ let zero = TCC(h: 00, m: 00, s: 00, f: 00)
+ .toTimecode(at: $0, limit: ._100days)!
+
+ let target = TCC(d: 99, h: 23, m: 59, s: 59, f: $0.maxFrameNumberDisplayable)
+ .toTimecode(at: $0, limit: ._100days)!
+
+ let delta = zero.distance(to: target)
+
+ XCTAssertEqual(delta, $0.maxTotalFramesExpressible(in: ._100days), "for \($0)")
+
+ }
+
+ }
+
+
+ // MARK: Integration Tests
+
+ func testOTTimecode_Strideable_Ranges() {
+ // Stride through & array
+
+ let strideThrough = stride(from: "01:00:00:00".toTimecode(at: ._23_976)!, through: "01:00:00:06".toTimecode(at: ._23_976)!, by: 2)
+ var array = Array(strideThrough)
+ XCTAssertEqual(array.count, 4)
+ XCTAssertEqual(array, ["01:00:00:00".toTimecode(at: ._23_976)!, "01:00:00:02".toTimecode(at: ._23_976)!, "01:00:00:04".toTimecode(at: ._23_976)!, "01:00:00:06".toTimecode(at: ._23_976)!])
+
+
+ // Stride to
+ let strideTo = stride(from: "01:00:00:00".toTimecode(at: ._23_976)!, to: "01:00:00:06".toTimecode(at: ._23_976)!, by: 2)
+ array = Array(strideTo)
+ XCTAssertEqual(array.count, 3)
+ XCTAssertEqual(array, ["01:00:00:00".toTimecode(at: ._23_976)!, "01:00:00:02".toTimecode(at: ._23_976)!, "01:00:00:04".toTimecode(at: ._23_976)!])
+
+
+ // Strideable
+
+ XCTAssertEqual("01:00:00:00".toTimecode(at: ._23_976)!.advanced(by: 6),
+ "01:00:00:06".toTimecode(at: ._23_976)!)
+
+ XCTAssertEqual("01:00:00:00".toTimecode(at: ._23_976)!.distance(to: "02:00:00:00".toTimecode(at: ._23_976)!),
+ Int("01:00:00:00".toTimecode(at: ._23_976)!.totalElapsedFrames))
+
+ let strs = Array(
+ stride(from: "01:00:00:05".toTimecode(at: ._23_976)!, through: "01:00:10:05".toTimecode(at: ._23_976)!,
+ by: Int(OTTimecode(TCC(s: 1), at: ._23_976)!.totalElapsedFrames))
+ ).map({ $0.stringValue })
+ XCTAssertEqual(strs.count, 11)
+
+ let strs2 = Array(
+ stride(from: "01:00:00:05".toTimecode(at: ._23_976)!, to: "01:00:10:07".toTimecode(at: ._23_976)!,
+ by: Int(OTTimecode(TCC(s: 1), at: ._23_976)!.totalElapsedFrames))
+ ).map({ $0.stringValue })
+ XCTAssertEqual(strs2.count, 11)
+
+ // Strideable with drop-frames
+
+ // TODO: ***** add strideable drop-frame tests
+
+ // Range .contains
+
+ XCTAssertTrue(("01:00:00:00".toTimecode(at: ._23_976)!..."01:00:00:06".toTimecode(at: ._23_976)!).contains(OTTimecode("01:00:00:02", at: ._23_976)!))
+ XCTAssertFalse(("01:00:00:00".toTimecode(at: ._23_976)!..."01:00:00:06".toTimecode(at: ._23_976)!).contains(OTTimecode("01:00:00:10", at: ._23_976)!))
+ XCTAssertTrue(("01:00:00:00".toTimecode(at: ._23_976)!...).contains(OTTimecode("01:00:00:02", at: ._23_976)!))
+ XCTAssertTrue((..."01:00:00:06".toTimecode(at: ._23_976)!).contains(OTTimecode("01:00:00:02", at: ._23_976)!))
+
+ // (same tests, but with ~= operator instead of .contains(...) which should produce the same result)
+
+ XCTAssertTrue("01:00:00:00".toTimecode(at: ._23_976)!..."01:00:00:06".toTimecode(at: ._23_976)! ~= OTTimecode("01:00:00:02", at: ._23_976)!)
+ XCTAssertFalse("01:00:00:00".toTimecode(at: ._23_976)!..."01:00:00:06".toTimecode(at: ._23_976)! ~= OTTimecode("01:00:00:10", at: ._23_976)!)
+ XCTAssertTrue("01:00:00:00".toTimecode(at: ._23_976)!... ~= OTTimecode("01:00:00:02", at: ._23_976)!)
+ XCTAssertTrue(..."01:00:00:06".toTimecode(at: ._23_976)! ~= OTTimecode("01:00:00:02", at: ._23_976)!)
+
+ // sort
+
+ let sort = ["01:00:00:06".toTimecode(at: ._23_976)!, "01:00:00:00".toTimecode(at: ._23_976)!].sorted()
+ XCTAssertEqual(sort[0], "01:00:00:00".toTimecode(at: ._23_976)!)
+
+ }
+
+}