orchetect/TimecodeKit

Add methods to use a `Timecode` struct as a template

Closed this issue · 2 comments

Proposal

In practise, it is often needed to create a new Timecode instance from a value but retain all of the options set on it.

For example, create a new Timecode instance from a realTimeValue (TimeInterval) without having to copy over all of its properties individually that were set (assuming some or all were not defaults).

// set up with non-default options
let tc1 = try Timecode("01:20:15:10",
                       at: ._29_97,
                       limit: ._100days
                       base: ._80SubFrames
                       format: [.showsSubFrames]
)

// currently, if you want to grandfather some or all of the properties
// to a new `Timecode` instance with a new time value,
// you have to copy them over individually:
let tc2 = try Timecode("01:20:15:10",
                       at: tc1.frameRate
                       limit: ._24hours // new upperLimit
                       base: tc1.subFramesBase
                       format: tc1.stringformat
)

When using Timecode in a codebase that deals heavily in manipulating timecode, this API starts to become very redundant and clutter up the code.

Solution 1

As referenced in #34, making all stored properties in Timecode mutable would allow the following:

var tc2 = tc1
tc2.upperLimit = ._24hours

Thus allowing you to use a "reference" or "template" instance of Timecode to derive all its configured properties from while changing one or more attributes, reducing the boilerplate needed.

However, this still requires one to first make a copy then mutate the property/properties.

Also, it does not come with the benefit of using an initializer that may be needed to validate the values with the new parameter(s).

Solution 2

In addition to Solution 1, also introduce a parallel API to add functional methods to create a copy of the instance while changing one or more parameters. Under the hood, they would essentially copy the Timecode struct, then run the corresponding setter on the copy, then return that copy.

This would also be widely useful in scenarios where you receive an immutable Timecode instance and you want to produce a copy with a value modified without needing to create an immutable var copy first.

// parallel `func .settingX() → Timecode` methods to each existing `mutating func .setX()` method

// returns an instance copy if `.setTimecode(:String)` on the copy succeeds.
let tc2 = tc1.settingTimecode("01:20:15:10")

Alternatively, it could be a single method with multiple defaulted parameters.

    /// Creates a new instance by copying the current instance with specified modifications.
    /// If `nil` is passed, that parameter will be inherited from the current instance.
    public func setting(rate: FrameRate? = nil,
                        limit: UpperLimit? = nil,
                        base: SubFramesBase? = nil,
                        format: StringFormat? = nil) -> Timecode
}

This could also inform a complete set of functional methods replicating all of Timecode's inits.

    /// Creates a new instance by copying the current instance with specified modifications.
    /// If `nil` is passed, that parameter will be inherited from the current instance.
    public func setting(_ exactly: FrameCount.Value,
                        at rate: FrameRate? = nil,
                        limit: UpperLimit? = nil,
                        base: SubFramesBase? = nil,
                        format: StringFormat? = nil) -> 

    /// Creates a new instance by copying the current instance with specified modifications.
    /// If `nil` is passed, that parameter will be inherited from the current instance.
    public func setting(_ exactly: FrameCount,
                        at rate: FrameRate? = nil,
                        limit: UpperLimit? = nil,
                        format: StringFormat? = nil) -> Timecode

    /// Creates a new instance by copying the current instance with specified modifications.
    /// If `nil` is passed, that parameter will be inherited from the current instance.
    public func setting(_ exactly: Components,
                        at rate: FrameRate? = nil,
                        limit: UpperLimit? = nil,
                        base: SubFramesBase? = nil,
                        format: StringFormat? = nil) -> Timecode

    /// Creates a new instance by copying the current instance with specified modifications.
    /// If `nil` is passed, that parameter will be inherited from the current instance.
    public func setting(_ exactly: String,
                        at rate: FrameRate? = nil,
                        limit: UpperLimit? = nil,
                        base: SubFramesBase? = nil,
                        format: StringFormat? = nil) -> Timecode

    /// Creates a new instance by copying the current instance with specified modifications.
    /// If `nil` is passed, that parameter will be inherited from the current instance.
    public func setting(realTimeValue: TimeInterval,
                        at rate: FrameRate? = nil,
                        limit: UpperLimit? = nil,
                        base: SubFramesBase? = nil,
                        format: StringFormat? = nil) -> Timecode

    /// Creates a new instance by copying the current instance with specified modifications.
    /// If `nil` is passed, that parameter will be inherited from the current instance.
    public func setting(samples: Double,
                        atSampleRate: Int? = nil,
                        at rate: FrameRate? = nil,
                        limit: UpperLimit? = nil,
                        base: SubFramesBase? = nil,
                        format: StringFormat? = nil) -> Timecode

It is also possible to add an initializer that takes another Timecode instance as a parameter. Its properties would all be inherited except for non-nil parameters.

extension Timecode {
    public init(from: Timecode,
                rate: FrameRate? = nil,
                limit: UpperLimit? = nil,
                base: SubFramesBase? = nil,
                format: StringFormat? = nil) -> Timecode
}

Upon further reflection, the most common use case would likely be setting new time component values while retaining rate, limit, base and format.

In that case, each current initializer could receive an overload that takes a Timecode instance as a template.

let tcTemplate = Timecode(
    at: ._29_97,
    limit: ._100days
    base: ._80SubFrames
    format: [.showsSubFrames]
)

let tc2 = try Timecode("01:20:15:10", template: tcTemplate)
// tc2.frameRate == ._29_97
// tc2.limit == ._100days
// tc2.subFramesBase == ._80SubFrames
// tc2.stringFormat == [.showsSubFrames]

This is addressed in the forthcoming 2.0.0 release.

A new Timecode.Properties struct that contains frameRate, limit, and base can be passed to Timecode initializers and various other methods.