CutBox.app

Coverage Report

Created: 2024-03-12 03:40

.../Source/App/TimeFilter/TimeFilterValidator.swift
Line
Count
Source (jump to first uncovered line)
1
//
2
//  TimeFilterValidator.swift
3
//  CutBox
4
//
5
//  Created by Jason Milkins on 13/8/23.
6
//  Copyright © 2023 ocodo. All rights reserved.
7
//
8
9
import Foundation
10
11
class TimeFilterValidator {
12
    private static let MINUTE: Int = 60
13
    private static let HOUR: Int = 3600
14
    private static let DAY: Int = 86400
15
    private static let WEEK: Int = 604800
16
    private static let YEAR: Int = 31536000
17
18
    typealias TimeUnitLabels = [(name: String, plural: String)]
19
20
    private static let secondsToTimeFull: TimeUnitLabels = [
21
        (name: "year", plural: "years"),
22
        (name: "week", plural: "weeks"),
23
        (name: "day", plural: "days"),
24
        (name: "hour", plural: "hours"),
25
        (name: "minute", plural: "minutes"),
26
        (name: "second", plural: "seconds")
27
    ]
28
29
    private static let secondsToTimeAbbreviated: TimeUnitLabels = [
30
        (name: "yr", plural: "yrs"),
31
        (name: "wk", plural: "wks"),
32
        (name: "d", plural: "d"),
33
        (name: "hr", plural: "hrs"),
34
        (name: "min", plural: "mins"),
35
        (name: "s", plural: "s")
36
    ]
37
38
12
    static func secondsToTime(seconds: Int, labels: TimeUnitLabels = secondsToTimeFull) -> String {
39
12
        let secondsToComponents = [
40
12
            seconds / YEAR,
41
12
            (seconds % YEAR) / WEEK,
42
12
            (seconds % WEEK) / DAY,
43
12
            (seconds % DAY) / HOUR,
44
12
            (seconds % HOUR) / MINUTE,
45
12
            seconds % MINUTE
46
12
        ]
47
12
48
72
        let components: [String] = zip(secondsToComponents, labels).map { value, label in
49
72
            switch value {
50
72
            case 1:
51
12
                return "\(Int(value)) \(label.name)"
52
72
            case 0:
53
46
                return ""
54
72
            default:
55
14
                return "\(Int(value)) \(label.plural)"
56
72
            }
57
72
        }
58
12
59
12
        if components.isEmpty {
60
0
            return "0 seconds"
61
12
        }
62
12
63
12
        var captured = ""
64
45
        for component in components where !component.isEmpty {
65
45
            if let index = components.firstIndex(of: component) {
66
12
                if components.count > index + 1 {
67
12
                    let adjacent = components[index + 1]
68
12
                    if adjacent.isEmpty {
69
5
                        captured = component
70
12
                    } else {
71
7
                        captured = "\(component) \(adjacent)"
72
12
                    }
73
12
                } else {
74
0
                    captured = component
75
12
                }
76
12
                break
77
33
            }
78
33
        }
79
12
80
12
        return captured
81
12
    }
82
83
    private static let timeUnitsTable = [
84
        (pattern: #"^(\d+(?:\.\d+)?) ?(m|minutes|min|mins|minute)$"#, factor: 60.0),
85
        (pattern: #"^(\d+(?:\.\d+)?) ?(h|hours|hr|hrs|hour)$"#, factor: 3600.0),
86
        (pattern: #"^(\d+(?:\.\d+)?) ?(d|days|day)$"#, factor: 86400.0),
87
        (pattern: #"^(\d+(?:\.\d+)?) ?(w|week|weeks|wk|wks)$"#, factor: 604800.0),
88
        (pattern: #"^(\d+(?:\.\d+)?) ?(y|year|years|yr|yrs)$"#, factor: 31536000.0),
89
        (pattern: #"^(\d+(?:\.\d+)?) ?(s|sec|secs|second|seconds)$"#, factor: 1.0)
90
    ]
91
92
150
    private static func filterNums(_ string: String) -> Double? {
93
812
        return Double(string.filter { $0 == "." || $0 >= "0" && $0 <= "9" })
94
150
    }
95
96
    /// Parse string to optional time interval. Any non-numeric chars will be
97
    /// filtered out after matching on seconds (or s,sec,secs), minutes (or
98
    /// m,min,mins), hours (or h,hr,hrs), days (or d,day). (case insensitive)
99
150
    private static func parseToSeconds(_ time: String) -> Double? {
100
150
        if let num = filterNums(time) {
101
612
            return timeUnitsTable.compactMap { (unit: (pattern: String, factor: Double)) -> Double? in
102
612
                if regexpMatch(time, unit.pattern, caseSensitive: false) {
103
101
                    return num * unit.factor
104
511
                }
105
511
                return nil
106
612
            }.first
107
102
        }
108
48
        return nil
109
150
    }
110
111
    /// Perform a regular expression (pattern) match on string, defaults to case sensitive.
112
    /// On match return true
113
612
    private static func regexpMatch(_ string: String, _ pattern: String, caseSensitive: Bool = true) -> Bool {
114
612
        let range = NSRange(location: 0, length: string.utf16.count)
115
612
        if caseSensitive {
116
0
            if let regex = try? NSRegularExpression(pattern: pattern) {
117
0
                return regex.firstMatch(in: string, options: [], range: range) != nil
118
0
            }
119
612
        } else {
120
612
            let regexOptions: NSRegularExpression.Options = [.caseInsensitive]
121
612
            if let regex = try? NSRegularExpression(pattern: pattern, options: regexOptions) {
122
612
                return regex.firstMatch(in: string, options: [], range: range) != nil
123
612
            }
124
0
        }
125
0
        return false
126
612
    }
127
128
    private let value: String
129
    let seconds: Double?
130
131
150
    var isValid: Bool {
132
150
        return self.seconds != nil
133
150
    }
134
135
153
    init(value: String) {
136
153
        self.value = value
137
153
        switch value {
138
153
        case _ where "today" == value.localizedLowercase:
139
1
            self.seconds = Double(Self.DAY)
140
153
        case _ where "this week" == value.localizedLowercase:
141
1
            self.seconds = Double(Self.WEEK)
142
153
        case _ where "yesterday" == value.localizedLowercase:
143
1
            self.seconds = 2 * Double(Self.DAY)
144
153
        default:
145
150
            self.seconds = Self.parseToSeconds(value)
146
153
        }
147
153
    }
148
}