# 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

    }