├── .editorconfig
├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── .spi.yml
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── contents.xcworkspacedata
├── CODE_OF_CONDUCT.md
├── Images
├── Apps
│ ├── CrossCraft.webp
│ ├── FocusBeats.webp
│ ├── FreelanceKit.webp
│ ├── FreemiumKit.webp
│ ├── GuidedGuestMode.webp
│ ├── PleydiaOrganizer.webp
│ ├── Posters.webp
│ └── TranslateKit.webp
├── Docs.webp
└── HandySwift.png
├── LICENSE
├── Package.swift
├── README.md
├── Sources
└── HandySwift
│ ├── Extensions
│ ├── ArrayExt.swift
│ ├── CaseIterableExt.swift
│ ├── CollectionExt.swift
│ ├── ComparableExt.swift
│ ├── DataExt.swift
│ ├── DateExt.swift
│ ├── DictionaryExt.swift
│ ├── DispatchTimeIntervalExt.swift
│ ├── DoubleExt.swift
│ ├── DurationExt.swift
│ ├── FloatExt.swift
│ ├── IntExt.swift
│ ├── JSONDecoderExt.swift
│ ├── JSONEncoderExt.swift
│ ├── NSRangeExt.swift
│ ├── RandomAccessCollectionExt.swift
│ ├── SequenceExt.swift
│ ├── StringExt.swift
│ ├── StringProtocolExt.swift
│ ├── SymmetricKeyExt.swift
│ └── TimeIntervalExt.swift
│ ├── Globals.swift
│ ├── HandySwift.docc
│ ├── Essentials
│ │ ├── Extensions.md
│ │ └── New Types.md
│ ├── HandySwift.md
│ ├── Resources
│ │ ├── Extensions.jpeg
│ │ ├── Extensions
│ │ │ ├── APIKeys.png
│ │ │ ├── CrosswordGeneration.png
│ │ │ ├── MusicPlayer.jpeg
│ │ │ ├── PremiumPlanExpires.png
│ │ │ ├── ProgressBar.jpeg
│ │ │ └── SharePuzzle.png
│ │ ├── HandySwift.png
│ │ └── NewTypes.jpeg
│ └── theme-settings.json
│ ├── Protocols
│ ├── AutoConforming.swift
│ ├── DivisibleArithmetic.swift
│ └── Withable.swift
│ └── Types
│ ├── Debouncer.swift
│ ├── FrequencyTable.swift
│ ├── GregorianDay.swift
│ ├── GregorianTime.swift
│ ├── HandyRegex.swift
│ ├── OperatingSystem.swift
│ ├── RESTClient.swift
│ └── SortedArray.swift
└── Tests
└── HandySwiftTests
├── Extensions
├── ArrayExtTests.swift
├── CollectionExtTests.swift
├── ComparableExtTests.swift
├── DictionaryExtTests.swift
├── DispatchTimeIntervalExtTests.swift
├── DoubleExtTests.swift
├── FloatExtTests.swift
├── IntExtTests.swift
├── NSObjectExtTests.swift
├── NSRangeExtTests.swift
├── StringExtTests.swift
└── TimeIntervalExtTests.swift
├── Protocols
└── WithableTests.swift
└── Structs
├── FrequencyTableTests.swift
├── GregorianDayTests.swift
├── HandyRegexTests.swift
└── SortedArrayTests.swift
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: https://editorconfig.org
2 | root = true
3 |
4 | [*]
5 |
6 | indent_style = space
7 | tab_width = 6
8 | indent_size = 3
9 |
10 | end_of_line = lf
11 | insert_final_newline = true
12 |
13 | max_line_length = 160
14 | trim_trailing_whitespace = true
15 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [main, versions]
6 |
7 | pull_request:
8 | branches: [main]
9 |
10 | jobs:
11 | test-linux:
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v4
16 |
17 | - name: Run tests
18 | run: swift test
19 |
20 | test-macos:
21 | runs-on: macos-15
22 |
23 | steps:
24 | - uses: actions/checkout@v4
25 |
26 | - name: Run tests
27 | run: swift test
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # macOS
2 |
3 | # General
4 | .DS_Store
5 | .AppleDouble
6 | .LSOverride
7 |
8 | # Icon must end with two \r
9 | Icon
10 |
11 | # Thumbnails
12 | ._*
13 |
14 | # Files that might appear in the root of a volume
15 | .DocumentRevisions-V100
16 | .fseventsd
17 | .Spotlight-V100
18 | .TemporaryItems
19 | .Trashes
20 | .VolumeIcon.icns
21 | .com.apple.timemachine.donotpresent
22 |
23 | # Directories potentially created on remote AFP share
24 | .AppleDB
25 | .AppleDesktop
26 | Network Trash Folder
27 | Temporary Items
28 | .apdisk
29 |
30 | # Xcode
31 | #
32 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
33 |
34 | ## Build generated
35 | build/
36 | DerivedData
37 |
38 | ## Various settings
39 | *.pbxuser
40 | !default.pbxuser
41 | *.mode1v3
42 | !default.mode1v3
43 | *.mode2v3
44 | !default.mode2v3
45 | *.perspectivev3
46 | !default.perspectivev3
47 | xcuserdata
48 |
49 | ## Other
50 | *.xccheckout
51 | *.moved-aside
52 | *.xcuserstate
53 | *.xcscmblueprint
54 |
55 | ## Obj-C/Swift specific
56 | *.hmap
57 | *.ipa
58 |
59 | # Swift Package Manager
60 | #
61 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
62 | # Packages/
63 | .build/
64 |
65 | # CocoaPods
66 | #
67 | # We recommend against adding the Pods directory to your .gitignore. However
68 | # you should judge for yourself, the pros and cons are mentioned at:
69 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
70 | #
71 | # Pods/
72 |
73 | # Carthage
74 | #
75 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
76 | # Carthage/Checkouts
77 |
78 | Carthage/Build
79 |
80 | # fastlane
81 | #
82 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
83 | # screenshots whenever they are needed.
84 | # For more information about the recommended setup visit:
85 | # https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md
86 |
87 | fastlane/report.xml
88 | fastlane/screenshots
89 | HandySwift.framework.zip
90 | docs/
91 |
--------------------------------------------------------------------------------
/.spi.yml:
--------------------------------------------------------------------------------
1 | version: 1
2 | builder:
3 | configs:
4 | - documentation_targets: [HandySwift]
5 | swift_version: 6.0
6 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, gender identity and expression, level of experience,
9 | nationality, personal appearance, race, religion, or sexual identity and
10 | orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at github [at] fline [dot] dev. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at [http://contributor-covenant.org/version/1/4][version]
72 |
73 | [homepage]: http://contributor-covenant.org
74 | [version]: http://contributor-covenant.org/version/1/4/
75 |
--------------------------------------------------------------------------------
/Images/Apps/CrossCraft.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlineDev/HandySwift/e9defa22f6c0f3ae529b6256375d679061f830ee/Images/Apps/CrossCraft.webp
--------------------------------------------------------------------------------
/Images/Apps/FocusBeats.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlineDev/HandySwift/e9defa22f6c0f3ae529b6256375d679061f830ee/Images/Apps/FocusBeats.webp
--------------------------------------------------------------------------------
/Images/Apps/FreelanceKit.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlineDev/HandySwift/e9defa22f6c0f3ae529b6256375d679061f830ee/Images/Apps/FreelanceKit.webp
--------------------------------------------------------------------------------
/Images/Apps/FreemiumKit.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlineDev/HandySwift/e9defa22f6c0f3ae529b6256375d679061f830ee/Images/Apps/FreemiumKit.webp
--------------------------------------------------------------------------------
/Images/Apps/GuidedGuestMode.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlineDev/HandySwift/e9defa22f6c0f3ae529b6256375d679061f830ee/Images/Apps/GuidedGuestMode.webp
--------------------------------------------------------------------------------
/Images/Apps/PleydiaOrganizer.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlineDev/HandySwift/e9defa22f6c0f3ae529b6256375d679061f830ee/Images/Apps/PleydiaOrganizer.webp
--------------------------------------------------------------------------------
/Images/Apps/Posters.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlineDev/HandySwift/e9defa22f6c0f3ae529b6256375d679061f830ee/Images/Apps/Posters.webp
--------------------------------------------------------------------------------
/Images/Apps/TranslateKit.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlineDev/HandySwift/e9defa22f6c0f3ae529b6256375d679061f830ee/Images/Apps/TranslateKit.webp
--------------------------------------------------------------------------------
/Images/Docs.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlineDev/HandySwift/e9defa22f6c0f3ae529b6256375d679061f830ee/Images/Docs.webp
--------------------------------------------------------------------------------
/Images/HandySwift.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlineDev/HandySwift/e9defa22f6c0f3ae529b6256375d679061f830ee/Images/HandySwift.png
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Copyright (c) 2015-2024 FlineDev (alias Cihat Gündüz)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 6.0
2 | import PackageDescription
3 |
4 | let package = Package(
5 | name: "HandySwift",
6 | platforms: [.iOS(.v12), .macOS(.v10_14), .tvOS(.v13), .visionOS(.v1), .watchOS(.v6)],
7 | products: [.library(name: "HandySwift", targets: ["HandySwift"])],
8 | targets: [
9 | .target(name: "HandySwift"),
10 | .testTarget(name: "HandySwiftTests", dependencies: ["HandySwift"]),
11 | ]
12 | )
13 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | [](https://swiftpackageindex.com/FlineDev/HandySwift)
4 |
5 | # HandySwift
6 |
7 | The goal of this library is to **provide handy features** that didn't make it into the Swift standard library (yet).
8 |
9 | Checkout [HandySwiftUI](https://github.com/FlineDev/HandySwiftUI) for handy UI features that should have been part of SwiftUI in the first place.
10 |
11 |
12 | ## Documentation
13 |
14 | Learn how you can make the most of HandySwift by reading the guides inside the documentation:
15 |
16 | [📖 Open HandySwift Documentation](https://swiftpackageindex.com/FlineDev/HandySwift/documentation/handyswift)
17 |
18 |
19 |
20 |
21 |
22 |
23 | ## Showcase
24 |
25 | I extracted this library from my following Indie apps (rate them with 5 stars to thank me!):
26 |
27 |
28 |
29 | App Icon |
30 | App Name & Description |
31 | Supported Platforms |
32 |
33 |
34 |
35 |
36 |
37 |
38 | |
39 |
40 |
41 | TranslateKit: App Localizer
42 |
43 |
44 | Simple drag & drop translation of String Catalog files with support for multiple translation services & smart correctness checks.
45 | |
46 | Mac |
47 |
48 |
49 |
50 |
51 |
52 |
53 | |
54 |
55 |
56 | Pleydia Organizer: Movie & Series Renamer
57 |
58 |
59 | Simple, fast, and smart media management for your Movie, TV Show and Anime collection.
60 | |
61 | Mac |
62 |
63 |
64 |
65 |
66 |
67 |
68 | |
69 |
70 |
71 | FreemiumKit: In-App Purchases
72 |
73 |
74 | Simple In-App Purchases and Subscriptions for Apple Platforms: Automation, Paywalls, A/B Testing, Live Notifications, PPP, and more.
75 | |
76 | iPhone, iPad, Mac, Vision |
77 |
78 |
79 |
80 |
81 |
82 |
83 | |
84 |
85 |
86 | FreelanceKit: Time Tracking
87 |
88 |
89 | Simple & affordable time tracking with a native experience for all devices. iCloud sync & CSV export included.
90 | |
91 | iPhone, iPad, Mac, Vision |
92 |
93 |
94 |
95 |
96 |
97 |
98 | |
99 |
100 |
101 | CrossCraft: Custom Crosswords
102 |
103 |
104 | Create themed & personalized crosswords. Solve them yourself or share them to challenge others.
105 | |
106 | iPhone, iPad, Mac, Vision |
107 |
108 |
109 |
110 |
111 |
112 |
113 | |
114 |
115 |
116 | FocusBeats: Pomodoro + Music
117 |
118 |
119 | Deep Focus with proven Pomodoro method & select Apple Music playlists & themes. Automatically pauses music during breaks.
120 | |
121 | iPhone, iPad, Mac, Vision |
122 |
123 |
124 |
125 |
126 |
127 |
128 | |
129 |
130 |
131 | Guided Guest Mode
132 |
133 |
134 | Showcase Apple Vision Pro effortlessly to friends & family. Customizable, easy-to-use guides for everyone!
135 | |
136 | Vision |
137 |
138 |
139 |
140 |
141 |
142 |
143 | |
144 |
145 |
146 | Posters: Discover Movies at Home
147 |
148 |
149 | Auto-updating & interactive posters for your home with trailers, showtimes, and links to streaming services.
150 | |
151 | Vision |
152 |
153 |
154 |
--------------------------------------------------------------------------------
/Sources/HandySwift/Extensions/ArrayExt.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | // - MARK: Migration
4 | extension Array {
5 | @available(*, unavailable, renamed: "sort(by:)", message: "Since SE-0372 shipped in Swift 5.8 `sort(by:)` is officially stable. Just remove the `stable` parameter.")
6 | public mutating func sort(by areInIncreasingOrder: @escaping (Element, Element) -> Bool, stable: Bool) { fatalError() }
7 |
8 | @available(*, unavailable, renamed: "sorted(by:)", message: "Since SE-0372 shipped in Swift 5.8 `sorted(by:)` is officially stable. Just remove the `stable` parameter.")
9 | public func sorted(by areInIncreasingOrder: @escaping (Element, Element) -> Bool, stable: Bool) -> [Element] { fatalError() }
10 | }
11 |
12 | extension Array where Element: Comparable {
13 | @available(*, unavailable, renamed: "sort()", message: "Since SE-0372 shipped in Swift 5.8 `sort()` is officially stable. Just remove the `stable` parameter.")
14 | public mutating func sort(stable: Bool) { fatalError() }
15 |
16 | @available(*, unavailable, renamed: "sorted()", message: "Since SE-0372 shipped in Swift 5.8 `sorted()` is officially stable. Just remove the `stable` parameter.")
17 | public func sorted(stable: Bool) -> [Element] { fatalError() }
18 | }
19 |
--------------------------------------------------------------------------------
/Sources/HandySwift/Extensions/CaseIterableExt.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension CaseIterable {
4 | /// Returns an array containing all cases of the conforming type, with `nil` prefixed.
5 | /// This can be especially useful in SwiftUI Pickers where you want to offer an option
6 | /// to not select any value. For example, in a Picker view for selecting an optional
7 | /// category, prefixing with `nil` allows users to explicitly select 'None' as an option.
8 | ///
9 | /// Example for SwiftUI:
10 | /// ```swift
11 | /// enum Category: String, CaseIterable {
12 | /// case books, movies, music
13 | /// }
14 | ///
15 | /// @State private var selectedCategory: Category? = nil
16 | ///
17 | /// var body: some View {
18 | /// Picker("Category", selection: $selectedCategory) {
19 | /// Text("None").tag(Category?.none)
20 | /// ForEach(Category.allCasesPrefixedByNil, id: \.self) { category in
21 | /// Text(category?.rawValue.capitalized ?? "None").tag(category as Category?)
22 | /// }
23 | /// }
24 | /// }
25 | /// ```
26 | /// - Returns: An array of optional values including `nil` followed by all cases of the type.
27 | public static var allCasesPrefixedByNil: [Self?] {
28 | [.none] + self.allCases.map(Optional.init)
29 | }
30 |
31 | /// Returns an array containing all cases of the conforming type, with `nil` suffixed.
32 | /// While similar to `allCasesPrefixedByNil`, this variation is useful when the 'None'
33 | /// option is more logically placed at the end of the list in the UI. This can align with
34 | /// user expectations in certain contexts, where selecting 'None' or 'Not specified' is
35 | /// considered an action taken after reviewing all available options.
36 | ///
37 | /// Example for SwiftUI:
38 | /// ```swift
39 | /// enum Level: String, CaseIterable {
40 | /// case beginner, intermediate, advanced
41 | /// }
42 | ///
43 | /// @State private var selectedLevel: Level? = nil
44 | ///
45 | /// var body: some View {
46 | /// Picker("Level", selection: $selectedLevel) {
47 | /// ForEach(Level.allCasesSuffixedByNil, id: \.self) { level in
48 | /// Text(level?.rawValue.capitalized ?? "None").tag(level as Level?)
49 | /// }
50 | /// }
51 | /// }
52 | /// ```
53 | /// - Returns: An array of optional values including all cases of the type followed by `nil`.
54 | public static var allCasesSuffixedByNil: [Self?] {
55 | self.allCases.map(Optional.init) + [.none]
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Sources/HandySwift/Extensions/CollectionExt.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension Collection {
4 | /// Returns the element at the specified index or `nil` if no element at the given index exists.
5 | /// This is particularly useful when dealing with collections of elements where you're unsure if an index is within the collection's bounds.
6 | /// It helps prevent crashes due to accessing an index out of bounds.
7 | ///
8 | /// Example:
9 | /// ```swift
10 | /// let testArray = [0, 1, 2, 3, 20]
11 | /// let element = testArray[safe: 4] // => Optional(20)
12 | /// let outOfBoundsElement = testArray[safe: 20] // => nil
13 | /// ```
14 | ///
15 | /// - Parameter index: The index of the element to retrieve.
16 | /// - Returns: The element at the specified index, or `nil` if the index is out of bounds.
17 | @inlinable
18 | public subscript(safe index: Index) -> Element? {
19 | self.indices.contains(index) ? self[index] : nil
20 | }
21 |
22 | /// Splits the collection into smaller chunks of the specified size.
23 | ///
24 | /// This method is useful for processing large datasets in manageable pieces, such as logging data, sending network requests, or performing batch updates.
25 | ///
26 | /// - Parameter size: The size of each chunk. Must be greater than 0.
27 | /// - Returns: An array of arrays, where each subarray represents a chunk of elements. The last chunk may contain fewer elements if the collection cannot be evenly divided.
28 | ///
29 | /// - Example:
30 | /// ```swift
31 | /// let numbers = Array(1...10)
32 | /// let chunks = numbers.chunks(ofSize: 3)
33 | /// print(chunks)
34 | /// // Output: [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]
35 | /// ```
36 | ///
37 | /// - Example: Processing items in chunks
38 | /// ```swift
39 | /// let tasks = ["Task1", "Task2", "Task3", "Task4"]
40 | /// for chunk in tasks.chunks(ofSize: 2) {
41 | /// for task in chunk {
42 | /// task.perform()
43 | /// }
44 | ///
45 | /// print("Processed chunk of tasks: \(chunk)")
46 | /// }
47 | /// // Output:
48 | /// // Processed chunk of tasks: ["Task1", "Task2"]
49 | /// // Processed chunk of tasks: ["Task3", "Task4"]
50 | /// ```
51 | public func chunks(ofSize size: Int) -> [[Element]] {
52 | guard size > 0 else { fatalError("Chunk size must be greater than 0.") }
53 |
54 | var result: [[Element]] = []
55 | var chunk: [Element] = []
56 | var count = 0
57 |
58 | for element in self {
59 | chunk.append(element)
60 | count += 1
61 | if count == size {
62 | result.append(chunk)
63 | chunk.removeAll(keepingCapacity: true)
64 | count = 0
65 | }
66 | }
67 |
68 | if !chunk.isEmpty {
69 | result.append(chunk)
70 | }
71 |
72 | return result
73 | }
74 | }
75 |
76 | extension Collection where Element: DivisibleArithmetic {
77 | /// Returns the average of all elements. It sums up all the elements and then divides by the count of the collection.
78 | /// This method requires that the element type conforms to `DivisibleArithmetic`, which includes numeric types such as `Int`, `Double`, etc.
79 | ///
80 | /// Example:
81 | /// ```swift
82 | /// let numbers: [Double] = [10.75, 20.75, 30.25, 40.25]
83 | /// let averageValue = numbers.average() // => 25.5
84 | /// ```
85 | ///
86 | /// - Returns: The average value of all elements in the collection.
87 | @inlinable
88 | public func average() -> Element {
89 | self.sum() / Element(self.count)
90 | }
91 | }
92 |
93 | extension Collection where Element == Int {
94 | /// Returns the average of all elements as a Double value.
95 | /// This is useful for `Int` collections where the precision of the average calculation is important and you prefer the result to be a `Double`.
96 | ///
97 | /// Example:
98 | /// ```swift
99 | /// let numbers = [10, 20, 30, 40]
100 | /// let averageValue: Double = numbers.average() // => 25.0
101 | /// ```
102 | ///
103 | /// - Returns: The average value of all elements in the collection as a Double.
104 | @inlinable
105 | public func average() -> ReturnType {
106 | ReturnType(self.sum()) / ReturnType(self.count)
107 | }
108 | }
109 |
110 | // MARK: Migration
111 | extension Collection {
112 | @available(*, unavailable, renamed: "subscript(safe:)")
113 | public subscript(try index: Index) -> Element? { fatalError() }
114 | }
115 |
--------------------------------------------------------------------------------
/Sources/HandySwift/Extensions/ComparableExt.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension Comparable {
4 | /// Returns `self` clamped to the given closed range limits.
5 | /// This method ensures that the value remains within a specific range.
6 | /// If the value is outside the range, it's adjusted to the nearest boundary of the range.
7 | ///
8 | /// Example:
9 | /// ```swift
10 | /// let myNum = 3
11 | /// let clampedNum = myNum.clamped(to: 0 ... 6) // => 3
12 | /// let clampedNumBelow = myNum.clamped(to: 0 ... 2) // => 2
13 | /// let clampedNumAbove = myNum.clamped(to: 4 ... 6) // => 4
14 | /// ```
15 | ///
16 | /// - Parameter limits: The closed range determining the minimum and maximum value.
17 | /// - Returns:
18 | /// - `self`, if it is inside the given limits.
19 | /// - `lowerBound` of the given limits, if `self` is smaller than it.
20 | /// - `upperBound` of the given limits, if `self` is greater than it.
21 | @inlinable
22 | public func clamped(to limits: ClosedRange) -> Self {
23 | if limits.lowerBound > self {
24 | limits.lowerBound
25 | } else if limits.upperBound < self {
26 | limits.upperBound
27 | } else {
28 | self
29 | }
30 | }
31 |
32 | /// Returns `self` clamped to the given partial range (from) limits. This method ensures that the value does not fall below a specified minimum.
33 | ///
34 | /// Example:
35 | /// ```swift
36 | /// let myNum = 3
37 | /// let clampedNum = myNum.clamped(to: 5...) // => 5
38 | /// ```
39 | ///
40 | /// - Parameter limits: The partial range (from) determining the minimum value.
41 | /// - Returns:
42 | /// - `self`, if it is inside the given limits.
43 | /// - `lowerBound` of the given limits, if `self` is smaller than it.
44 | @inlinable
45 | public func clamped(to limits: PartialRangeFrom) -> Self {
46 | limits.lowerBound > self ? limits.lowerBound : self
47 | }
48 |
49 | /// Returns `self` clamped to the given partial range (through) limits.
50 | /// This method ensures that the value does not exceed a specified maximum.
51 | ///
52 | /// Example:
53 | /// ```swift
54 | /// let myNum = 7
55 | /// let clampedNum = myNum.clamped(to: ...5) // => 5
56 | /// ```
57 | ///
58 | /// - Parameter limits: The partial range (through) determining the maximum value.
59 | /// - Returns:
60 | /// - `self`, if it is inside the given limits.
61 | /// - `upperBound` of the given limits, if `self` is greater than it.
62 | @inlinable
63 | public func clamped(to limits: PartialRangeThrough) -> Self {
64 | limits.upperBound < self ? limits.upperBound : self
65 | }
66 |
67 | /// Clamps `self` to the given closed range limits.
68 | /// Modifies the original value to ensure it falls within a specific range, adjusting it to the nearest boundary if necessary.
69 | ///
70 | /// Example:
71 | /// ```swift
72 | /// var myNum = 3
73 | /// myNum.clamp(to: 0...2)
74 | /// print(myNum) // => 2
75 | /// ```
76 | ///
77 | /// - Parameter limits: The closed range determining minimum and maximum value.
78 | @inlinable
79 | public mutating func clamp(to limits: ClosedRange) {
80 | self = clamped(to: limits)
81 | }
82 |
83 | /// Clamps `self` to the given partial range (from) limits.
84 | /// Modifies the original value to ensure it does not fall below a specified minimum.
85 | ///
86 | /// Example:
87 | /// ```swift
88 | /// var myNum = 3
89 | /// myNum.clamp(to: 5...)
90 | /// print(myNum) // => 5
91 | /// ```
92 | ///
93 | /// - Parameter limits: The partial range (from) determining the minimum value.
94 | @inlinable
95 | public mutating func clamp(to limits: PartialRangeFrom) {
96 | self = clamped(to: limits)
97 | }
98 |
99 | /// Clamps `self` to the given partial range (through) limits.
100 | /// Modifies the original value to ensure it does not exceed a specified maximum.
101 | ///
102 | /// Example:
103 | /// ```swift
104 | /// var myNum = 7
105 | /// myNum.clamp(to: ...5)
106 | /// print(myNum) // => 5
107 | /// ```
108 | ///
109 | /// - `self`, if it is inside the given limits.
110 | /// - `upperBound` of the given limits, if `self` is greater than it.
111 | ///
112 | /// - Parameter limits: The partial range (through) determining the maximum value.
113 | @inlinable
114 | public mutating func clamp(to limits: PartialRangeThrough) {
115 | self = clamped(to: limits)
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/Sources/HandySwift/Extensions/DataExt.swift:
--------------------------------------------------------------------------------
1 | #if canImport(CryptoKit)
2 | import CryptoKit
3 | import Foundation
4 |
5 | extension Data {
6 | /// Encrypts this plain `Data` using AES.GCM with the provided key.
7 | /// This method is useful for encrypting data before securely storing or transmitting it.
8 | /// Ensure that the `SymmetricKey` used for encryption is securely managed and stored.
9 | ///
10 | /// Example:
11 | /// ```swift
12 | /// let key = SymmetricKey(size: .bits256)
13 | /// let plainData = "Harry Potter is a 🧙".data(using: .utf8)!
14 | /// do {
15 | /// let encryptedData = try plainData.encrypted(key: key)
16 | /// // Use encryptedData as needed
17 | /// } catch {
18 | /// print("Encryption failed: \(error)")
19 | /// }
20 | /// ```
21 | ///
22 | /// - Parameters:
23 | /// - key: The symmetric key to use for encryption.
24 | /// - Returns: The encrypted `Data`.
25 | /// - Throws: An error if encryption fails.
26 | /// - Note: Available on iOS 13, macOS 10.15, tvOS 13, visionOS 1, watchOS 6, and later.
27 | @available(iOS 13, macOS 10.15, tvOS 13, visionOS 1, watchOS 6, *)
28 | public func encrypted(key: SymmetricKey) throws -> Data {
29 | try AES.GCM.seal(self, using: key).combined!
30 | }
31 |
32 | /// Decrypts this encrypted data using AES.GCM with the provided key.
33 | /// This method is crucial for converting encrypted data back to its original form securely.
34 | /// Ensure the `SymmetricKey` used matches the one used for encryption.
35 | ///
36 | /// Example:
37 | /// ```swift
38 | /// let key = SymmetricKey(size: .bits256)
39 | /// // Assuming encryptedData is the Data we previously encrypted
40 | /// do {
41 | /// let decryptedData = try encryptedData.decrypted(key: key)
42 | /// let decryptedString = String(data: decryptedData, encoding: .utf8)!
43 | /// // Use decryptedString or decryptedData as needed
44 | /// } catch {
45 | /// print("Decryption failed: \(error)")
46 | /// }
47 | /// ```
48 | ///
49 | /// - Parameters:
50 | /// - key: The symmetric key to use for decryption.
51 | /// - Returns: The decrypted `Data`.
52 | /// - Throws: An error if decryption fails.
53 | /// - Note: Available on iOS 13, macOS 10.15, tvOS 13, visionOS 1, watchOS 6, and later.
54 | @available(iOS 13, macOS 10.15, tvOS 13, visionOS 1, watchOS 6, *)
55 | public func decrypted(key: SymmetricKey) throws -> Data {
56 | let sealedBox = try AES.GCM.SealedBox(combined: self)
57 | return try AES.GCM.open(sealedBox, using: key)
58 | }
59 | }
60 | #endif
61 |
--------------------------------------------------------------------------------
/Sources/HandySwift/Extensions/DateExt.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension Date {
4 | /// Creates a new Date by combining a ``GregorianDay`` with a ``GregorianTime``.
5 | ///
6 | /// This initializer allows you to create a Date instance from separate day and time components.
7 | /// The resulting date will be in the specified timezone (defaulting to the current timezone).
8 | ///
9 | /// Example:
10 | /// ```swift
11 | /// let day = GregorianDay(year: 2024, month: 3, day: 21)
12 | /// let time = GregorianTime(hour: 14, minute: 30)
13 | /// let eventTime = Date(day: day, time: time)
14 | /// ```
15 | ///
16 | /// - Parameters:
17 | /// - day: The GregorianDay representing the date components.
18 | /// - time: The GregorianTime representing the time components.
19 | /// - timeZone: The timezone to use for the date creation. Defaults to the current timezone.
20 | public init(day: GregorianDay, time: GregorianTime, timeZone: TimeZone = .current) {
21 | self = time.date(day: day, timeZone: timeZone)
22 | }
23 |
24 | /// Returns a date offset by the specified time interval from this date to the past.
25 | /// This method is useful when you need to calculate a date that occurred a certain duration before the current date instance.
26 | /// For example, if you want to find out what the date was 2 hours ago from a given date, you can use this method by passing the time interval for 2 hours in seconds.
27 | ///
28 | /// Example:
29 | /// ```swift
30 | /// let now = Date()
31 | /// let twoHoursInSeconds: TimeInterval = 2 * 60 * 60
32 | /// let twoHoursAgo = now.reversed(by: twoHoursInSeconds)
33 | /// print("Two hours ago: \(twoHoursAgo)")
34 | /// ```
35 | ///
36 | /// - Parameter interval: The time interval offset to subtract from this date.
37 | /// - Returns: A date offset by subtracting the specified time interval from this date.
38 | @available(iOS 13, macOS 10.15, tvOS 13, visionOS 1, watchOS 6, *)
39 | public func reversed(by interval: TimeInterval) -> Date {
40 | self.advanced(by: -interval)
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Sources/HandySwift/Extensions/DictionaryExt.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension Dictionary {
4 | /// Initializes a new `Dictionary` and populates it with keys and values arrays.
5 | /// This method is particularly useful when you have separate arrays of keys and values and you need to combine them into a single dictionary.
6 | /// It ensures that each key is mapped to its corresponding value based on their order in the arrays.
7 | ///
8 | /// Example:
9 | /// ```swift
10 | /// let names = ["firstName", "lastName"]
11 | /// let values = ["Harry", "Potter"]
12 | /// let person = Dictionary(keys: names, values: values)
13 | /// // => ["firstName": "Harry", "lastName": "Potter"]
14 | /// ```
15 | ///
16 | /// - Parameters:
17 | /// - keys: An array containing keys to be added to the dictionary.
18 | /// - values: An array containing values corresponding to the keys.
19 | ///
20 | /// - Requires: The number of elements in `keys` must be equal to the number of elements in `values`.
21 | ///
22 | /// - Returns: A new dictionary initialized with the provided keys and values arrays, or `nil` if the number of elements in the arrays differs.
23 | @inlinable
24 | public init?(keys: [Key], values: [Value]) {
25 | guard keys.count == values.count else { return nil }
26 | self.init()
27 | for (index, key) in keys.enumerated() { self[key] = values[index] }
28 | }
29 |
30 | /// Transforms the keys of the dictionary using the given closure, returning a new dictionary with the transformed keys.
31 | ///
32 | /// - Parameter transform: A closure that takes a key from the dictionary as its argument and returns a new key.
33 | /// - Returns: A dictionary with keys transformed by the `transform` closure and the same values as the original dictionary.
34 | /// - Throws: Rethrows any error thrown by the `transform` closure.
35 | ///
36 | /// - Warning: If the `transform` closure produces duplicate keys, the values of earlier keys will be overridden by the values of later keys in the resulting dictionary.
37 | ///
38 | /// - Example:
39 | /// ```
40 | /// let originalDict = ["one": 1, "two": 2, "three": 3]
41 | /// let transformedDict = originalDict.mapKeys { $0.uppercased() }
42 | /// // transformedDict will be ["ONE": 1, "TWO": 2, "THREE": 3]
43 | /// ```
44 | @inlinable
45 | public func mapKeys(_ transform: (Key) throws -> K) rethrows -> [K: Value] {
46 | var transformedDict: [K: Value] = [:]
47 | for (key, value) in self {
48 | transformedDict[try transform(key)] = value
49 | }
50 | return transformedDict
51 | }
52 | }
53 |
54 | // - MARK: Migration
55 | extension Dictionary {
56 | @available(*, unavailable, renamed: "merge(_:uniquingKeysWith:)", message: "Append `{ $1 }` as a `uniquingKeysWith` trailing closure to migrate.")
57 | public mutating func merge(_ other: [Key: Value]) { fatalError() }
58 |
59 | @available(*, unavailable, renamed: "merging(_:uniquingKeysWith:)", message: "Remove the `with:` label and append `{ $1 }` as a `uniquingKeysWith` trailing closure to migrate.")
60 | public func merged(with other: [Key: Value]) -> [Key: Value] { fatalError() }
61 | }
62 |
--------------------------------------------------------------------------------
/Sources/HandySwift/Extensions/DispatchTimeIntervalExt.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension DispatchTimeInterval {
4 | /// Converts the dispatch time interval to seconds using the `TimeInterval` type.
5 | /// This is useful for when you need to work with `DispatchTimeInterval` values in contexts that require `TimeInterval` (in seconds),
6 | /// such as scheduling timers, animations, or any operations that are based on seconds.
7 | ///
8 | /// Example:
9 | /// ```swift
10 | /// let delay = DispatchTimeInterval.seconds(5)
11 | /// Timer.scheduledTimer(withTimeInterval: delay.timeInterval, repeats: false) { _ in
12 | /// print("Timer fired after 5 seconds.")
13 | /// }
14 | /// ```
15 | ///
16 | /// - Returns: The time interval in seconds. For `.never`, returns `TimeInterval.infinity` to represent an indefinite time interval.
17 | public var timeInterval: TimeInterval {
18 | switch self {
19 | case let .seconds(seconds):
20 | Double(seconds)
21 |
22 | case let .milliseconds(milliseconds):
23 | Double(milliseconds) / TimeInterval.millisecondsPerSecond
24 |
25 | case let .microseconds(microseconds):
26 | Double(microseconds) / TimeInterval.microsecondsPerSecond
27 |
28 | case let .nanoseconds(nanoseconds):
29 | Double(nanoseconds) / TimeInterval.nanosecondsPerSecond
30 |
31 | case .never:
32 | TimeInterval.infinity
33 |
34 | @unknown default:
35 | fatalError("Unknown DispatchTimeInterval unit.")
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/HandySwift/Extensions/DoubleExt.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension Double {
4 | /// Rounds the value to an integral value using the specified number of fraction digits and rounding rule.
5 | /// This is useful for when you need precise control over the rounding behavior of floating-point calculations,
6 | /// such as in financial calculations where rounding to a specific number of decimal places is required.
7 | ///
8 | /// Example:
9 | /// ```swift
10 | /// var price: Double = 2.875
11 | /// price.round(fractionDigits: 2) // price becomes 2.88
12 | ///
13 | /// // Using a different rounding rule:
14 | /// price.round(fractionDigits: 2, rule: .down) // price becomes 2.87
15 | /// ```
16 | ///
17 | /// - Parameters:
18 | /// - fractionDigits: The number of fraction digits to round to.
19 | /// - rule: The rounding rule to apply. Default is `.toNearestOrAwayFromZero`.
20 | ///
21 | /// - Note: Dropping the `rule` parameter will default to “schoolbook rounding”.
22 | public mutating func round(fractionDigits: Int, rule: FloatingPointRoundingRule = .toNearestOrAwayFromZero) {
23 | let divisor = pow(10.0, Double(fractionDigits))
24 | self = (self * divisor).rounded(rule) / divisor
25 | }
26 |
27 | /// Returns this value rounded to an integral value using the specified number of fraction digits and rounding rule.
28 | /// This method does not mutate the original value but instead returns a new `Double` that is the result of the rounding operation,
29 | /// making it suitable for cases where the original value must remain unchanged.
30 | ///
31 | /// Example:
32 | /// ```swift
33 | /// let price: Double = 2.875
34 | /// let roundedPrice = price.rounded(fractionDigits: 2) // => 2.88
35 | ///
36 | /// // Using a different rounding rule:
37 | /// let roundedDownPrice = price.rounded(fractionDigits: 2, rule: .down) // => 2.87
38 | /// ```
39 | ///
40 | /// - Parameters:
41 | /// - fractionDigits: The number of fraction digits to round to.
42 | /// - rule: The rounding rule to apply. Default is `.toNearestOrAwayFromZero`.
43 | ///
44 | /// - Note: Dropping the `rule` parameter will default to “schoolbook rounding”.
45 | ///
46 | /// - Returns: The rounded value.
47 | public func rounded(fractionDigits: Int, rule: FloatingPointRoundingRule = .toNearestOrAwayFromZero) -> Double {
48 | let divisor = pow(10.0, Double(fractionDigits))
49 | return (self * divisor).rounded(rule) / divisor
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Sources/HandySwift/Extensions/DurationExt.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | @available(iOS 16, macOS 13, tvOS 16, visionOS 1, watchOS 9, *)
4 | extension Duration {
5 | /// Returns the duration as a `TimeInterval`.
6 | ///
7 | /// This can be useful for interfacing with APIs that require `TimeInterval` (which is measured in seconds), allowing you to convert a `Duration` directly to the needed format.
8 | ///
9 | /// Example:
10 | /// ```swift
11 | /// let duration = Duration.hours(2)
12 | /// let timeInterval = duration.timeInterval // Converts to TimeInterval for compatibility
13 | /// ```
14 | ///
15 | /// - Returns: The duration as a `TimeInterval`, which represents the duration in seconds.
16 | public var timeInterval: TimeInterval {
17 | TimeInterval(self.components.seconds) + (TimeInterval(self.components.attoseconds) / 1_000_000_000_000_000_000)
18 | }
19 |
20 | /// Constructs a `Duration` given a number of weeks represented as a `BinaryInteger`.
21 | ///
22 | /// This method can be particularly useful when working with durations that span several weeks, such as project timelines or subscription periods.
23 | ///
24 | /// Example:
25 | /// ```swift
26 | /// let threeWeeksDuration = Duration.weeks(3) // Creates a Duration of 3 weeks
27 | /// ```
28 | ///
29 | /// - Parameter weeks: The number of weeks.
30 | /// - Returns: A `Duration` representing the given number of weeks.
31 | public static func weeks(_ weeks: T) -> Duration {
32 | self.days(weeks * 7)
33 | }
34 |
35 | /// Constructs a `Duration` given a number of days represented as a `BinaryInteger`.
36 | ///
37 | /// Useful for creating durations for events, reminders, or deadlines that are a specific number of days in the future.
38 | ///
39 | /// Example:
40 | /// ```swift
41 | /// let tenDaysDuration = Duration.days(10) // Creates a Duration of 10 days
42 | /// ```
43 | ///
44 | /// - Parameter days: The number of days.
45 | /// - Returns: A `Duration` representing the given number of days.
46 | public static func days(_ days: T) -> Duration {
47 | self.hours(days * 24)
48 | }
49 |
50 | /// Constructs a `Duration` given a number of hours represented as a `BinaryInteger`.
51 | ///
52 | /// Can be used to schedule events or tasks that are several hours long.
53 | ///
54 | /// Example:
55 | /// ```swift
56 | /// let eightHoursDuration = Duration.hours(8) // Creates a Duration of 8 hours
57 | /// ```
58 | ///
59 | /// - Parameter hours: The number of hours.
60 | /// - Returns: A `Duration` representing the given number of hours.
61 | public static func hours(_ hours: T) -> Duration {
62 | self.minutes(hours * 60)
63 | }
64 |
65 | /// Constructs a `Duration` given a number of minutes represented as a `BinaryInteger`.
66 | ///
67 | /// This is helpful for precise time measurements, such as cooking timers, short breaks, or meeting durations.
68 | ///
69 | /// Example:
70 | /// ```swift
71 | /// let fifteenMinutesDuration = Duration.minutes(15) // Creates a Duration of 15 minutes
72 | /// ```
73 | ///
74 | /// - Parameter minutes: The number of minutes.
75 | /// - Returns: A `Duration` representing the given number of minutes.
76 | public static func minutes(_ minutes: T) -> Duration {
77 | self.seconds(minutes * 60)
78 | }
79 |
80 | /// Formats the duration to a human-readable string with auto-scaling.
81 | ///
82 | /// This function takes a duration and formats it into a string that represents the duration in the largest non-zero unit, scaling from seconds up to days.
83 | /// The output is dynamically scaled to provide a succinct and easily understandable representation of the duration.
84 | ///
85 | /// Examples:
86 | /// - For a duration of 45 seconds, the output will be `"45s"`.
87 | /// - For a duration of 150 minutes, the output will be `"2h 30m"` as it scales up from minutes to hours and minutes.
88 | /// - For a duration of 25 hours, the output will be `"1d 1h"`, scaling up to days and hours.
89 | /// - For a duration of 2 days and 0 hours, the output will be `"2d"` as it omits the zero hour part.
90 | ///
91 | /// - Returns: A formatted string representing the duration. If the duration is less than a second, it returns `"???"`. For zero returns `"0s"`.
92 | public func autoscaleFormatted() -> String {
93 | guard self != .zero else { return "0s" }
94 |
95 | var leftoverTimeInterval = self.timeInterval
96 | let fullDays = Int(leftoverTimeInterval.days)
97 |
98 | leftoverTimeInterval -= .days(Double(fullDays))
99 | let fullHours = Int(leftoverTimeInterval.hours)
100 |
101 | leftoverTimeInterval -= .hours(Double(fullHours))
102 | let fullMinutes = Int(leftoverTimeInterval.minutes)
103 |
104 | leftoverTimeInterval -= .minutes(Double(fullMinutes))
105 | let fullSeconds = Int(leftoverTimeInterval.seconds)
106 |
107 | if fullDays > 0 {
108 | guard fullHours != 0 else { return "\(fullDays)d" }
109 | return "\(fullDays)d \(fullHours)h"
110 | } else if fullHours > 0 {
111 | guard fullMinutes != 0 else { return "\(fullHours)h" }
112 | return "\(fullHours)h \(fullMinutes)m"
113 | } else if fullMinutes > 0 {
114 | guard fullSeconds != 0 else { return "\(fullMinutes)m" }
115 | return "\(fullMinutes)m \(fullSeconds)s"
116 | } else if fullSeconds > 0 {
117 | return "\(fullSeconds)s"
118 | } else {
119 | return "???"
120 | }
121 | }
122 | }
123 |
124 | @available(iOS 16, macOS 13, tvOS 16, visionOS 1, watchOS 9, *)
125 | extension Duration {
126 | /// Multiplies the duration by a given factor. This can be useful when scaling animations or timer intervals proportionally.
127 | ///
128 | /// Example:
129 | /// ```swift
130 | /// let originalDuration = Duration.seconds(2)
131 | /// let doubledDuration = originalDuration.multiplied(by: 2) // Doubles the duration to 4 seconds
132 | /// ```
133 | ///
134 | /// - Parameter factor: The multiplication factor.
135 | /// - Returns: A `Duration` representing the multiplied duration.
136 | public func multiplied(by factor: Double) -> Duration {
137 | (self.timeInterval * factor).duration()
138 | }
139 |
140 | /// Multiplies the duration by a given factor, allowing for integer factors. This is convenient for simple multiplications where decimal precision isn't needed.
141 | ///
142 | /// Example:
143 | /// ```swift
144 | /// let originalDuration = Duration.seconds(3)
145 | /// let tripledDuration = originalDuration.multiplied(by: 3) // Triples the duration to 9 seconds
146 | /// ```
147 | ///
148 | /// - Parameter factor: The multiplication factor.
149 | /// - Returns: A `Duration` representing the multiplied duration.
150 | public func multiplied(by factor: Int) -> Duration {
151 | self.multiplied(by: Double(factor))
152 | }
153 |
154 | /// Divides the duration by a given denominator, useful for reducing animation durations or calculating partial intervals.
155 | ///
156 | /// Example:
157 | /// ```swift
158 | /// let originalDuration = Duration.seconds(10)
159 | /// let halvedDuration = originalDuration.divided(by: 2) // Halves the duration to 5 seconds
160 | /// ```
161 | ///
162 | /// - Parameter denominator: The denominator.
163 | /// - Returns: A `Duration` representing the divided duration.
164 | public func divided(by denominator: Double) -> Duration {
165 | (self.timeInterval / denominator).duration()
166 | }
167 |
168 | /// Divides the duration by a given denominator using an integer value. This method simplifies division when working with whole numbers.
169 | ///
170 | /// Example:
171 | /// ```swift
172 | /// let originalDuration = Duration.seconds(8)
173 | /// let quarteredDuration = originalDuration.divided(by: 4) // Divides the duration to 2 seconds
174 | /// ```
175 | ///
176 | /// - Parameter denominator: The denominator.
177 | /// - Returns: A `Duration` representing the divided duration.
178 | public func divided(by denominator: Int) -> Duration {
179 | self.divided(by: Double(denominator))
180 | }
181 |
182 | /// Divides the duration by another duration, returning the ratio of the two. This can be used to compare durations or compute proportions.
183 | ///
184 | /// Example:
185 | /// ```swift
186 | /// let durationA = Duration.seconds(5)
187 | /// let durationB = Duration.seconds(2)
188 | /// let ratio = durationA.divided(by: durationB) // Calculates the ratio, which is 2.5
189 | /// ```
190 | ///
191 | /// - Parameter duration: The duration to divide by.
192 | /// - Returns: The ratio of the two durations.
193 | public func divided(by duration: Duration) -> Double {
194 | self.timeInterval / duration.timeInterval
195 | }
196 | }
197 |
--------------------------------------------------------------------------------
/Sources/HandySwift/Extensions/FloatExt.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension Float {
4 | /// Rounds the value to an integral value using the specified number of fraction digits and rounding rule.
5 | /// This is useful for when you need precise control over the formatting of floating-point numbers,
6 | /// for example, when displaying currency or other numerical calculations that require a specific number of decimal places.
7 | ///
8 | /// Example:
9 | /// ```swift
10 | /// var price: Float = 2.875
11 | /// price.round(fractionDigits: 2) // => 2.88
12 | ///
13 | /// // Using a specific rounding rule:
14 | /// price.round(fractionDigits: 2, rule: .down) // => 2.87
15 | /// ```
16 | ///
17 | /// - Parameters:
18 | /// - fractionDigits: The number of fraction digits to round to.
19 | /// - rule: The rounding rule to use. Defaults to `.toNearestOrAwayFromZero`.
20 | ///
21 | /// - Note: Dropping the `rule` parameter will default to “schoolbook rounding”.
22 | public mutating func round(fractionDigits: Int, rule: FloatingPointRoundingRule = .toNearestOrAwayFromZero) {
23 | let divisor = pow(10.0, Float(fractionDigits))
24 | self = (self * divisor).rounded(rule) / divisor
25 | }
26 |
27 | /// Returns this value rounded to an integral value using the specified number of fraction digits and rounding rule.
28 | /// Similar to `round`, but this method does not modify the original value and instead returns a new `Float` with the rounded value.
29 | /// This is particularly handy in functional programming paradigms where immutability is preferred.
30 | ///
31 | /// Example:
32 | /// ```swift
33 | /// let originalPrice: Float = 2.875
34 | /// let roundedPrice = originalPrice.rounded(fractionDigits: 2) // => 2.88
35 | ///
36 | /// // With explicit rounding rule:
37 | /// let roundedDownPrice = originalPrice.rounded(fractionDigits: 2, rule: .down) // => 2.87
38 | /// ```
39 | ///
40 | /// - Parameters:
41 | /// - fractionDigits: The number of fraction digits to round to.
42 | /// - rule: The rounding rule to use. Defaults to `.toNearestOrAwayFromZero`.
43 | ///
44 | /// - Note: Dropping the `rule` parameter will default to “schoolbook rounding”.
45 | public func rounded(fractionDigits: Int, rule: FloatingPointRoundingRule = .toNearestOrAwayFromZero) -> Float {
46 | let divisor = pow(10.0, Float(fractionDigits))
47 | return (self * divisor).rounded(rule) / divisor
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Sources/HandySwift/Extensions/IntExt.swift:
--------------------------------------------------------------------------------
1 | extension Int {
2 | /// Runs the code passed as a closure the specified number of times.
3 | /// This method is useful for repeating an action multiple times, such as logging a message or incrementing a value.
4 | /// It guards against negative values, ensuring the closure is only run for positive counts.
5 | ///
6 | /// Example:
7 | /// ```swift
8 | /// 3.times { print("Hello World!") }
9 | /// // This will print "Hello World!" 3 times.
10 | /// ```
11 | ///
12 | /// - Parameters:
13 | /// - closure: The code to be run multiple times.
14 | /// - Throws: Any error thrown by the closure.
15 | @inlinable
16 | public func times(_ closure: () throws -> Void) rethrows {
17 | guard self > 0 else { return }
18 | for _ in 0..(_ closure: () throws -> ReturnType) rethrows -> [ReturnType] {
36 | guard self > 0 else { return [] }
37 | return try (0..(randomBelow upperLimit: Int, using generator: inout Generator) { fatalError() }
48 | }
49 |
--------------------------------------------------------------------------------
/Sources/HandySwift/Extensions/JSONDecoderExt.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension JSONDecoder {
4 | /// A pre-configured JSONDecoder that automatically converts snake_case JSON property names
5 | /// to camelCase when decoding into Swift types
6 | ///
7 | /// This decoder handles incoming JSON with snake_case keys and converts them to match
8 | /// Swift's camelCase property naming convention.
9 | ///
10 | /// Example usage:
11 | /// ```
12 | /// let jsonString = """
13 | /// {
14 | /// "first_name": "John",
15 | /// "last_name": "Doe"
16 | /// }
17 | /// """
18 | /// let user = try JSONDecoder.snakeCase.decode(User.self, from: jsonData)
19 | /// // Results in: User(firstName: "John", lastName: "Doe")
20 | /// ```
21 | public static var snakeCase: JSONDecoder {
22 | let decoder = JSONDecoder()
23 | decoder.keyDecodingStrategy = .convertFromSnakeCase
24 | return decoder
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/HandySwift/Extensions/JSONEncoderExt.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension JSONEncoder {
4 | /// A pre-configured JSONEncoder that automatically converts Swift's camelCase property names
5 | /// to snake_case when encoding JSON
6 | ///
7 | /// Instead of creating a new encoder and configuring it each time, this provides a ready-to-use
8 | /// encoder with snake_case conversion.
9 | ///
10 | /// Example usage:
11 | /// ```
12 | /// let user = User(firstName: "John", lastName: "Doe")
13 | /// let jsonData = try JSONEncoder.snakeCase.encode(user)
14 | /// // Results in: {"first_name": "John", "last_name": "Doe"}
15 | /// ```
16 | public static var snakeCase: JSONEncoder {
17 | let encoder = JSONEncoder()
18 | encoder.keyEncodingStrategy = .convertToSnakeCase
19 | return encoder
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/HandySwift/Extensions/NSRangeExt.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension NSRange {
4 | /// Initializes an NSRange from a Swift String.Range when the String is provided.
5 | /// This is useful for operations that require NSRange, such as string manipulation with `NSRegularExpression`, or when working with UIKit components that deal with attributed strings.
6 | ///
7 | /// Example:
8 | /// ```swift
9 | /// let string = "Hello World!"
10 | /// let swiftRange = string.startIndex.., in string: String) {
20 | self.init()
21 | self.location = string.utf16.distance(from: string.startIndex, to: range.lowerBound)
22 | self.length = string.utf16.distance(from: range.lowerBound, to: range.upperBound)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/HandySwift/Extensions/RandomAccessCollectionExt.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension RandomAccessCollection where Index == Int {
4 | /// Returns a given number of random elements from the collection.
5 | /// This method is useful when you need a subset of elements for sampling, testing, or any other case where random selection from a collection is required.
6 | /// If the collection is empty, `nil` is returned instead.
7 | ///
8 | /// Example:
9 | /// ```swift
10 | /// let numbers = [1, 2, 3, 4, 5]
11 | /// if let randomNumbers = numbers.randomElements(count: 3) {
12 | /// print(randomNumbers)
13 | /// }
14 | /// // Output: [2, 1, 4] (example output, actual output will vary)
15 | /// ```
16 | ///
17 | /// - Parameters:
18 | /// - count: The number of random elements wanted.
19 | /// - Returns: An array with the given number of random elements or `nil` if the collection is empty.
20 | @inlinable
21 | public func randomElements(count: Int) -> [Element]? {
22 | guard !self.isEmpty else { return nil }
23 | return count.timesMake { self.randomElement()! }
24 | }
25 | }
26 |
27 | // - MARK: Migration
28 | extension RandomAccessCollection where Index == Int {
29 | @available(*, unavailable, renamed: "randomElement()")
30 | public var sample: Element? { fatalError() }
31 |
32 | @available(*, unavailable, renamed: "randomElements(count:)")
33 | public func sample(size: Int) -> [Element]? { fatalError() }
34 | }
35 |
--------------------------------------------------------------------------------
/Sources/HandySwift/Extensions/StringExt.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | #if canImport(CryptoKit)
3 | import CryptoKit
4 | #endif
5 |
6 | extension String {
7 | /// Checks if the string contains any characters other than whitespace or newline characters.
8 | /// This can be useful for validating input fields where a non-empty value is required.
9 | ///
10 | /// Example:
11 | /// ```swift
12 | /// " \t ".isBlank // => true
13 | /// "Hello".isBlank // => false
14 | /// ```
15 | ///
16 | /// - Returns: `true` if the string contains non-whitespace characters, `false` otherwise.
17 | public var isBlank: Bool { self.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }
18 |
19 | /// Returns the range containing the full string.
20 | /// Useful for operations that require a `Range`, such as modifications and substring extraction.
21 | ///
22 | /// Example:
23 | /// ```swift
24 | /// let unicodeString = "Hello composed unicode symbols! 👨👩👧👦👨👨👦👦👩👩👧👧"
25 | /// unicodeString[unicodeString.fullRange] // => same string
26 | /// ```
27 | ///
28 | /// - Returns: The range representing the full string.
29 | public var fullRange: Range {
30 | self.startIndex.. NSRange representing the full string
40 | /// ```
41 | ///
42 | /// - Returns: The NSRange representation of the full string range.
43 | public var fullNSRange: NSRange {
44 | NSRange(fullRange, in: self)
45 | }
46 |
47 | /// Creates a new instance with a random numeric/alphabetic/alphanumeric string of given length.
48 | /// This is useful for generating random identifiers, test data, or any scenario where random strings are needed.
49 | ///
50 | /// Examples:
51 | /// ```swift
52 | /// String(randomWithLength: 4, allowedCharactersType: .numeric) // => "8503"
53 | /// String(randomWithLength: 6, allowedCharactersType: .alphabetic) // => "ysTUzU"
54 | /// String(randomWithLength: 8, allowedCharactersType: .alphaNumeric) // => "2TgM5sUG"
55 | /// ```
56 | ///
57 | /// - Parameters:
58 | /// - randomWithLength: The length of the random string to create.
59 | /// - allowedCharactersType: The type of allowed characters, see enum ``AllowedCharacters``.
60 | public init(randomWithLength length: Int, allowedCharactersType: AllowedCharacters) {
61 | let allowedCharsString: String = {
62 | switch allowedCharactersType {
63 | case .numeric:
64 | return "0123456789"
65 |
66 | case .alphabetic:
67 | return "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
68 |
69 | case .alphaNumeric:
70 | return "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
71 |
72 | case let .allCharactersIn(allowedCharactersString):
73 | return allowedCharactersString
74 | }
75 | }()
76 |
77 | self.init(allowedCharsString.randomElements(count: length)!)
78 | }
79 |
80 | /// Returns a given number of random characters from the string. Useful for scenarios like sampling characters or generating substrings from a set of allowed characters.
81 | ///
82 | /// Example:
83 | /// ```swift
84 | /// let allowedChars = "abcdefghijklmnopqrstuvwxyz"
85 | /// let randomChars = allowedChars.randomElements(count: 5) // Example output: "xkqoi"
86 | /// ```
87 | ///
88 | /// - Parameters:
89 | /// - count: The number of random characters wanted.
90 | /// - Returns: A string with the given number of random characters or `nil` if empty.
91 | @inlinable
92 | public func randomElements(count: Int) -> String? {
93 | guard !self.isEmpty else { return nil }
94 | return String(count.timesMake { self.randomElement()! })
95 | }
96 | }
97 |
98 | extension String {
99 | /// The type of allowed characters.
100 | /// This is used in conjunction with `init(randomWithLength:allowedCharactersType:)` to specify the characters that can be included in the random string.
101 | public enum AllowedCharacters {
102 | /// Allow all numbers from 0 to 9. Useful for numeric identifiers or pin codes.
103 | case numeric
104 | /// Allow all alphabetic characters ignoring case. Useful for textual data where numbers are not needed.
105 | case alphabetic
106 | /// Allow both numbers and alphabetic characters ignoring case. Useful for alphanumeric identifiers.
107 | case alphaNumeric
108 | /// Allow all characters appearing within the specified string. This gives you full control over the characters that can appear in the random string.
109 | case allCharactersIn(String)
110 | }
111 | }
112 |
113 | #if canImport(CryptoKit)
114 | extension String {
115 | /// Error types that may occur during cryptographic operations.
116 | public enum CryptingError: LocalizedError {
117 | case convertingStringToDataFailed
118 | case decryptingDataFailed
119 | case convertingDataToStringFailed
120 |
121 | public var errorDescription: String? {
122 | switch self {
123 | case .convertingDataToStringFailed:
124 | return "Converting Data to String failed."
125 |
126 | case .decryptingDataFailed:
127 | return "Decrypting Data failed."
128 |
129 | case .convertingStringToDataFailed:
130 | return "Converting String to Data failed."
131 | }
132 | }
133 | }
134 |
135 | /// Encrypts this plain text `String` with the given key using AES.GCM and returns a base64 encoded representation of the encrypted data.
136 | /// This method is useful for securing sensitive information before storing or transmitting it.
137 | ///
138 | /// Example:
139 | /// ```swift
140 | /// let key = SymmetricKey(size: .bits256)
141 | /// let plainText = "Sensitive information"
142 | /// let encryptedString = try plainText.encrypted(key: key)
143 | /// print(encryptedString) // Encrypted base64 string
144 | /// ```
145 | ///
146 | /// - Parameter key: The symmetric key used for encryption.
147 | /// - Returns: A base64 encoded representation of the encrypted data.
148 | /// - Throws: A ``CryptingError`` if encryption fails.
149 | @available(iOS 13, macOS 10.15, tvOS 13, visionOS 1, watchOS 6, *)
150 | public func encrypted(key: SymmetricKey) throws -> String {
151 | guard let plainData = self.data(using: .utf8) else {
152 | throw CryptingError.convertingStringToDataFailed
153 | }
154 |
155 | let encryptedData = try plainData.encrypted(key: key)
156 | return encryptedData.base64EncodedString()
157 | }
158 |
159 | /// Decrypts this base64 encoded representation of encrypted data with the given key using AES.GCM and returns the decrypted plain text `String`.
160 | /// This method allows the secure transmission or storage of sensitive information to be reversed, returning the original plain text.
161 | ///
162 | /// Example:
163 | /// ```swift
164 | /// let key = SymmetricKey(size: .bits256)
165 | /// let encryptedString = "Base64EncodedEncryptedString"
166 | /// let decryptedString = try encryptedString.decrypted(key: key)
167 | /// print(decryptedString) // Original sensitive information
168 | /// ```
169 | ///
170 | /// - Parameter key: The symmetric key used for decryption.
171 | /// - Returns: The decrypted plain text `String`.
172 | /// - Throws: A ``CryptingError`` if decryption fails.
173 | @available(iOS 13, macOS 10.15, tvOS 13, visionOS 1, watchOS 6, *)
174 | public func decrypted(key: SymmetricKey) throws -> String {
175 | guard let encryptedData = Data(base64Encoded: self) else {
176 | throw CryptingError.decryptingDataFailed
177 | }
178 |
179 | let plainData = try encryptedData.decrypted(key: key)
180 | guard let plainString = String(data: plainData, encoding: .utf8) else {
181 | throw CryptingError.convertingDataToStringFailed
182 | }
183 |
184 | return plainString
185 | }
186 |
187 | /// Splits the String into word tokens that are folded for case-insensitive, diacritics-insensitive, and width-insensitive operations such as search.
188 | /// This is particularly useful for string normalization in search queries, where the goal is to match strings regardless of their case, diacritics, or full-width/half-width characters.
189 | ///
190 | /// - Parameter locale: Optional. The locale to use for the folding operation. If `nil`, the system's current locale is used. This affects the folding behavior, especially for diacritics.
191 | /// - Returns: An array of normalized, tokenized strings.
192 | ///
193 | /// ## Example:
194 | /// ```
195 | /// let sentence = "Café au lait"
196 | /// let tokens = sentence.tokenized()
197 | /// print(tokens) // Output: ["cafe", "au", "lait"]
198 | /// ```
199 | public func tokenized(locale: Locale? = nil) -> [String] {
200 | self.components(separatedBy: .whitespacesAndNewlines).map { word in
201 | word.folding(options: [.caseInsensitive, .diacriticInsensitive, .widthInsensitive], locale: locale)
202 | }
203 | }
204 |
205 | /// Splits both the current string and the search text into word tokens and performs a case-insensitive, diacritics-insensitive search.
206 | /// It matches the start of each token in the search text with the tokens in the current string, making it suitable for prefix-based search queries.
207 | ///
208 | /// - Parameters:
209 | /// - searchText: The text to search for within this String.
210 | /// - locale: Optional. The locale to use for the insensitivity folding operation. If `nil`, the system's current locale is used. This can impact how characters are folded for comparison.
211 | /// - Returns: `true` if all tokens from the search text are prefixes of any token in this String; otherwise, `false`.
212 | ///
213 | /// ## Example:
214 | /// ```
215 | /// let text = "Terms and Conditions"
216 | /// let searchResult = text.matchesTokenizedPrefixes(in: "ter con")
217 | /// print(searchResult) // Output: true
218 | /// ```
219 | public func matchesTokenizedPrefixes(in searchText: String, locale: Locale? = nil) -> Bool {
220 | let tokens = self.tokenized(locale: locale)
221 | return searchText.tokenized(locale: locale).allSatisfy { searchToken in
222 | tokens.contains { $0.hasPrefix(searchToken) }
223 | }
224 | }
225 | }
226 | #endif
227 |
228 |
229 | // - MARK: Migration
230 | extension String {
231 | @available(*, unavailable, renamed: "randomElement()")
232 | public var sample: Character? { fatalError() }
233 |
234 | @available(*, unavailable, renamed: "trimmingCharacters(in:)", message: "Pass `.whitespacesAndNewlines` to the functions `in` parameter for same behavior.")
235 | public func stripped() -> String { fatalError() }
236 |
237 | @available(*, unavailable, renamed: "randomElements(count:)")
238 | public func sample(size: Int) -> String? { fatalError() }
239 | }
240 |
--------------------------------------------------------------------------------
/Sources/HandySwift/Extensions/StringProtocolExt.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension StringProtocol {
4 | /// Returns a variation of the string with the first character uppercased.
5 | /// This is useful for formatting text that needs to start with a capital letter, such as titles or names, while preserving the case of the rest of the string.
6 | ///
7 | /// Example:
8 | /// ```swift
9 | /// let exampleString = "hello World"
10 | /// print(exampleString.firstUppercased) // Hello World
11 | /// ```
12 | ///
13 | /// - Returns: A string with the first character converted to uppercase.
14 | public var firstUppercased: String { self.prefix(1).uppercased() + self.dropFirst() }
15 |
16 | /// Returns a variation of the string with the first character lowercased.
17 | /// This can be useful in scenarios where a string starts with an uppercase letter but needs to be integrated into a sentence or phrase seamlessly.
18 | ///
19 | /// Example:
20 | /// ```swift
21 | /// let exampleString = "Hello world"
22 | /// print(exampleString.firstLowercased) // hello world
23 | /// ```
24 | ///
25 | /// - Returns: A string with the first character converted to lowercase and the rest unchanged.
26 | public var firstLowercased: String { self.prefix(1).lowercased() + self.dropFirst() }
27 | }
28 |
29 | // - MARK: Migration
30 | extension StringProtocol {
31 | @available(*, unavailable, renamed: "firstUppercased")
32 | public var firstCapitalized: String { fatalError() }
33 | }
34 |
--------------------------------------------------------------------------------
/Sources/HandySwift/Extensions/SymmetricKeyExt.swift:
--------------------------------------------------------------------------------
1 | #if canImport(CryptoKit)
2 | import Foundation
3 | import CryptoKit
4 |
5 | @available(iOS 13, macOS 10.15, tvOS 13, visionOS 1, watchOS 6, *)
6 | extension SymmetricKey {
7 | /// Returns a Base64-encoded string representation of the symmetric key. This can be useful for storing the key in a format that can be easily transmitted or stored.
8 | ///
9 | /// Example:
10 | /// ```swift
11 | /// let key = SymmetricKey(size: .bits256)
12 | /// let base64String = key.base64EncodedString
13 | /// // Now `base64String` can be stored or transmitted, and later used to recreate the SymmetricKey.
14 | /// ```
15 | ///
16 | /// - Returns: A Base64-encoded string representation of the symmetric key.
17 | public var base64EncodedString: String {
18 | withUnsafeBytes { Data($0).base64EncodedString() }
19 | }
20 |
21 | /// Initializes a symmetric key from a Base64-encoded string. This is particularly useful for reconstructing a symmetric key from a stored or transmitted Base64-encoded string.
22 | ///
23 | /// Example:
24 | /// ```swift
25 | /// let base64String = "your_base64_encoded_string_here"
26 | /// if let key = SymmetricKey(base64Encoded: base64String) {
27 | /// // Use `key` here.
28 | /// } else {
29 | /// // Handle the error: the provided string was not a valid Base64-encoded SymmetricKey.
30 | /// }
31 | /// ```
32 | ///
33 | /// - Parameter base64Encoded: The Base64-encoded string representing the symmetric key.
34 | /// - Returns: A symmetric key initialized from the Base64-encoded string, or `nil` if the input string is invalid.
35 | public init?(base64Encoded: String) {
36 | guard let data = Data(base64Encoded: base64Encoded) else { return nil }
37 | self.init(data: data)
38 | }
39 | }
40 | #endif
41 |
--------------------------------------------------------------------------------
/Sources/HandySwift/Extensions/TimeIntervalExt.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension TimeInterval {
4 | /// The number of seconds in a day.
5 | @usableFromInline
6 | internal static var secondsPerDay: Double { 24 * 60 * 60 }
7 |
8 | /// The number of seconds in an hour.
9 | @usableFromInline
10 | internal static var secondsPerHour: Double { 60 * 60 }
11 |
12 | /// The number of seconds in a minute.
13 | @usableFromInline
14 | internal static var secondsPerMinute: Double { 60 }
15 |
16 | /// The number of milliseconds in a second.
17 | @usableFromInline
18 | internal static var millisecondsPerSecond: Double { 1_000 }
19 |
20 | /// The number of microseconds in a second.
21 | @usableFromInline
22 | internal static var microsecondsPerSecond: Double { 1_000 * 1_000 }
23 |
24 | /// The number of nanoseconds in a second.
25 | @usableFromInline
26 | internal static var nanosecondsPerSecond: Double { 1_000 * 1_000 * 1_000 }
27 |
28 | /// Returns the time interval converted to days.
29 | ///
30 | /// - Returns: The `TimeInterval` in days.
31 | @inlinable
32 | public var days: Double {
33 | self / TimeInterval.secondsPerDay
34 | }
35 |
36 | /// Returns the time interval converted to hours.
37 | ///
38 | /// - Returns: The `TimeInterval` in hours.
39 | @inlinable
40 | public var hours: Double {
41 | self / TimeInterval.secondsPerHour
42 | }
43 |
44 | /// Returns the time interval converted to minutes.
45 | ///
46 | /// - Returns: The `TimeInterval` in minutes.
47 | @inlinable
48 | public var minutes: Double {
49 | self / TimeInterval.secondsPerMinute
50 | }
51 |
52 | /// Returns the time interval as is, representing seconds.
53 | ///
54 | /// - Returns: The `TimeInterval` in seconds.
55 | @inlinable
56 | public var seconds: Double {
57 | self
58 | }
59 |
60 | /// Returns the time interval converted to milliseconds.
61 | ///
62 | /// - Returns: The `TimeInterval` in milliseconds.
63 | @inlinable
64 | public var milliseconds: Double {
65 | self * TimeInterval.millisecondsPerSecond
66 | }
67 |
68 | /// Returns the time interval converted to microseconds.
69 | ///
70 | /// - Returns: The `TimeInterval` in microseconds.
71 | @inlinable
72 | public var microseconds: Double {
73 | self * TimeInterval.microsecondsPerSecond
74 | }
75 |
76 | /// Returns the time interval converted to nanoseconds.
77 | ///
78 | /// - Returns: The `TimeInterval` in nanoseconds.
79 | @inlinable
80 | public var nanoseconds: Double {
81 | self * TimeInterval.nanosecondsPerSecond
82 | }
83 |
84 | /// Converts the provided value to `TimeInterval` representing days.
85 | ///
86 | /// - Parameter value: The value to convert.
87 | /// - Returns: The time interval in days.
88 | @inlinable
89 | public static func days(_ value: Double) -> TimeInterval {
90 | value * secondsPerDay
91 | }
92 |
93 | /// Converts the provided value to `TimeInterval` representing hours.
94 | ///
95 | /// - Parameter value: The value to convert.
96 | /// - Returns: The time interval in hours.
97 | @inlinable
98 | public static func hours(_ value: Double) -> TimeInterval {
99 | value * secondsPerHour
100 | }
101 |
102 | /// Converts the provided value to `TimeInterval` representing minutes.
103 | ///
104 | /// - Parameter value: The value to convert.
105 | /// - Returns: The time interval in minutes.
106 | @inlinable
107 | public static func minutes(_ value: Double) -> TimeInterval {
108 | value * secondsPerMinute
109 | }
110 |
111 | /// Converts the provided value to `TimeInterval` representing seconds.
112 | ///
113 | /// - Parameter value: The value to convert.
114 | /// - Returns: The time interval in seconds.
115 | @inlinable
116 | public static func seconds(_ value: Double) -> TimeInterval {
117 | value
118 | }
119 |
120 | /// Converts the provided value to `TimeInterval` representing milliseconds.
121 | ///
122 | /// - Parameter value: The value to convert.
123 | /// - Returns: The time interval in milliseconds.
124 | @inlinable
125 | public static func milliseconds(_ value: Double) -> TimeInterval {
126 | value / millisecondsPerSecond
127 | }
128 |
129 | /// Converts the provided value to `TimeInterval` representing microseconds.
130 | ///
131 | /// - Parameter value: The value to convert.
132 | /// - Returns: The time interval in microseconds.
133 | @inlinable
134 | public static func microseconds(_ value: Double) -> TimeInterval {
135 | value / microsecondsPerSecond
136 | }
137 |
138 | /// Converts the provided value to `TimeInterval` representing nanoseconds.
139 | ///
140 | /// - Parameter value: The value to convert.
141 | /// - Returns: The time interval in nanoseconds.
142 | @inlinable
143 | public static func nanoseconds(_ value: Double) -> TimeInterval {
144 | value / nanosecondsPerSecond
145 | }
146 |
147 | /// Returns the `Duration` representation of the current time interval.
148 | ///
149 | /// - Returns: The `Duration` representation.
150 | @available(iOS 16, macOS 13, tvOS 16, visionOS 1, watchOS 9, *)
151 | public func duration() -> Duration {
152 | let fullSeconds = Int64(self.seconds)
153 | let remainingInterval = self - Double(fullSeconds)
154 |
155 | let attosecondsPerNanosecond = Double(1_000 * 1_000 * 1_000)
156 | let fullAttoseconds = Int64(remainingInterval.nanoseconds * attosecondsPerNanosecond)
157 |
158 | return Duration(secondsComponent: fullSeconds, attosecondsComponent: fullAttoseconds)
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/Sources/HandySwift/Globals.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// Runs code with a delay given in seconds, using the main thread by default or a specified QoS class.
4 | ///
5 | /// - Parameters:
6 | /// - timeInterval: The duration of the delay. E.g., `.seconds(1)` or `.milliseconds(200)`.
7 | /// - qosClass: The global QoS class to be used or `nil` to use the main thread. Defaults to `nil`.
8 | /// - closure: The code to run with a delay.
9 | public func delay(by timeInterval: TimeInterval, qosClass: DispatchQoS.QoSClass? = nil, _ closure: @Sendable @escaping () -> Void) {
10 | let dispatchQueue = qosClass != nil ? DispatchQueue.global(qos: qosClass!) : DispatchQueue.main
11 | dispatchQueue.asyncAfter(deadline: DispatchTime.now() + timeInterval, execute: closure)
12 | }
13 |
14 | /// Runs code with a delay given in seconds, using the main thread by default or a specified QoS class.
15 | ///
16 | /// - Parameters:
17 | /// - duration: The duration of the delay. E.g., `.seconds(1)` or `.milliseconds(200)`.
18 | /// - qosClass: The global QoS class to be used or `nil` to use the main thread. Defaults to `nil`.
19 | /// - closure: The code to run with a delay.
20 | @_disfavoredOverload
21 | @available(iOS 16, macOS 13, tvOS 16, visionOS 1, watchOS 9, *)
22 | public func delay(by duration: Duration, qosClass: DispatchQoS.QoSClass? = nil, _ closure: @Sendable @escaping () -> Void) {
23 | let dispatchQueue = qosClass != nil ? DispatchQueue.global(qos: qosClass!) : DispatchQueue.main
24 | dispatchQueue.asyncAfter(deadline: DispatchTime.now() + duration.timeInterval, execute: closure)
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/HandySwift/HandySwift.docc/Essentials/Extensions.md:
--------------------------------------------------------------------------------
1 | # Extensions
2 |
3 | Making existing types more convenient to use.
4 |
5 | @Metadata {
6 | @PageImage(purpose: icon, source: "HandySwift")
7 | @PageImage(purpose: card, source: "Extensions")
8 | }
9 |
10 | ## Highlights
11 |
12 | In the [Topics](#topics) section below you can find a list of all extension properties & functions. Click on one to reveal more details.
13 |
14 | To get you started quickly, here are the ones I use in nearly all of my apps with a practical usage example for each:
15 |
16 | #### Safe Index Access
17 |
18 | 
19 |
20 | In [FocusBeats][FocusBeats] I'm accessing an array of music tracks using an index. With ``Swift/Collection/subscript(safe:)`` I avoid out of bounds crashes:
21 |
22 | ```swift
23 | var nextEntry: ApplicationMusicPlayer.Queue.Entry? {
24 | guard let nextEntry = playerQueue.entries[safe: currentEntryIndex + 1] else { return nil }
25 | return nextEntry
26 | }
27 | ```
28 |
29 | You can use it on every type that conforms to ``Swift/Collection`` including `Array`, `Dictionary`, and `String`. Instead of calling the subscript `array[index]` which returns a non-Optional but crashes when the index is out of bounds, use the safer `array[safe: index]` which returns `nil` instead of crashing in those cases.
30 |
31 | #### Blank Strings vs Empty Strings
32 |
33 | 
34 |
35 | A common issue with text fields that are required to be non-empty is that users accidentally type a whitespace or newline character and don't recognize it. If the validation code just checks for `.isEmpty` the problem will go unnoticed. That's why in [TranslateKit][TranslateKit] when users enter an API key I make sure to first strip away any newlines and whitespaces from the beginning & end of the String before doing the `.isEmpty` check. And because this is something I do very often in many places, I wrote a helper:
36 |
37 | ```Swift
38 | Image(systemName: self.deepLAuthKey.isBlank ? "xmark.circle" : "checkmark.circle")
39 | .foregroundStyle(self.deepLAuthKey.isBlank ? .red : .green)
40 | ```
41 |
42 | Just use ``Swift/String/isBlank`` instead of `isEmpty` to get the same behavior!
43 |
44 | #### Readable Time Intervals
45 |
46 | 
47 |
48 | Whenever I used an API that expects a `TimeInterval` (which is just a typealias for `Double`), I missed the unit which lead to less readable code because you have to actively remember that the unit is "seconds". Also, when I needed a different unit like minutes or hours, I had to do the calculation manually. Not with HandySwift!
49 |
50 | Intead of passing a plain `Double` value like `60 * 5`, you can just pass `.minutes(5)`. For example in [TranslateKit][TranslateKit] to preview the view when a user unsubscribed I use this:
51 |
52 | ```swift
53 | #Preview("Expiring") {
54 | ContentView(
55 | hasPremiumAccess: true,
56 | premiumExpiresAt: Date.now.addingTimeInterval(.days(3))
57 | )
58 | }
59 | ```
60 |
61 | You can even chain multiple units with a `+` sign to create a day in time like "09:41 AM":
62 |
63 | ```swift
64 | let startOfDay = Calendar.current.startOfDay(for: Date.now)
65 | let iPhoneRevealedAt = startOfDay.addingTimeInterval(.hours(9) + .minutes(41))
66 | ```
67 |
68 | Note that this API design is in line with ``Swift/Duration`` and ``Dispatch/DispatchTimeInterval`` which both already support things like `.milliseconds(250)`. But they stop at the seconds level, they don't go higher. HandySwift adds minutes, hours, days, and even weeks for those types, too. So you can write something like this:
69 |
70 | ```swift
71 | try await Task.sleep(for: .minutes(5))
72 | ```
73 |
74 | > Warning: Advancing time by intervals does not take into account complexities like daylight saving time. Use a `Calendar` for that.
75 |
76 | #### Calculate Averages
77 |
78 | 
79 |
80 | In the crossword generation algorithm within [CrossCraft][CrossCraft] I have a health function on every iteration that calculates the overall quality of the puzzle. Two different aspects are taken into consideration:
81 |
82 | ```swift
83 | /// A value between 0 and 1.
84 | func calculateQuality() -> Double {
85 | let fieldCoverage = Double(solutionBoard.fields) / Double(maxFillableFields)
86 | let intersectionsCoverage = Double(solutionBoard.intersections) / Double(maxIntersections)
87 | return [fieldCoverage, intersectionsCoverage].average()
88 | }
89 | ```
90 |
91 | In previous versions I played around with different weights, for example giving intersections double the weight compared to field coverage. I could still achieve this using ``Swift/Collection/average()-3g44u`` like this in the last line:
92 |
93 | ```swift
94 | return [fieldCoverage, intersectionsCoverage, intersectionsCoverage].average()
95 | ```
96 |
97 | #### Round Floating-Point Numbers
98 |
99 | 
100 |
101 | When solving a puzzle in [CrossCraft][CrossCraft] you can see your current progress at the top of the screen. I use the built-in percent formatter (`.formatted(.percent)`) for numerics, but it requires a `Double` with a value between 0 and 1 (1 = 100%). Passing an `Int` like `12` unexpectedly renders as `0%`, so I can't simply do this:
102 | ```swift
103 | Int(fractionCompleted * 100).formatted(.percent) // => "0%" until "100%"
104 | ```
105 |
106 | And just doing `fractionCompleted.formatted(.percent)` results in sometimes very long text such as `"0.1428571429"`.
107 |
108 | Instead, I make use of ``Swift/Double/rounded(fractionDigits:rule:)`` to round the `Double` to 2 significant digits like so:
109 |
110 | ```swift
111 | Text(fractionCompleted.rounded(fractionDigits: 2).formatted(.percent))
112 | ```
113 |
114 | > Note: There's also a mutating ``Swift/Double/round(fractionDigits:rule:)`` functions if you want to change a variable in-place.
115 |
116 | #### JSON Snake Case Conversion
117 |
118 | Many APIs return responses using snake_case naming (e.g., `first_name`, `date_of_birth`), while Swift uses camelCase by convention (e.g., `firstName`, `dateOfBirth`). Converting between these naming conventions is a very common need when working with JSON APIs:
119 |
120 | ```swift
121 | struct User: Codable {
122 | let firstName: String
123 | let lastName: String
124 | let dateOfBirth: Date
125 | let profileImageUrl: String?
126 | }
127 |
128 | func fetchUser(id: String) async throws -> User {
129 | let (data, _) = try await URLSession.shared.data(from: apiURL)
130 |
131 | // Automatically converts snake_case JSON to camelCase Swift properties
132 | return try JSONDecoder.snakeCase.decode(User.self, from: data)
133 |
134 | // Without this extension we'd need this every time:
135 | // let decoder = JSONDecoder()
136 | // decoder.keyDecodingStrategy = .convertFromSnakeCase
137 | // return try decoder.decode(User.self, from: data)
138 | }
139 | ```
140 |
141 | Just use ``JSONDecoder.snakeCase`` to decode and ``JSONEncoder.snakeCase`` to encode instead of configuring a new instance each time!
142 |
143 | #### Symmetric Data Cryptography
144 |
145 | 
146 |
147 | Before uploading a crossword puzzle in [CrossCraft][CrossCraft] I make sure to encrypt it so tech-savvy people can't easily sniff the answers from the JSON like so:
148 |
149 | ```swift
150 | func upload(puzzle: Puzzle) async throws {
151 | let key = SymmetricKey(base64Encoded: "")!
152 | let plainData = try JSONEncoder().encode(puzzle)
153 | let encryptedData = try plainData.encrypted(key: key)
154 |
155 | // upload logic
156 | }
157 | ```
158 |
159 | Note that the above code makes use of two extensions, first ``CryptoKit/SymmetricKey/init(base64Encoded:)`` is used to initialize the key, then ``Foundation/Data/encrypted(key:)`` encrypts the data using safe ``CryptoKit`` APIs internally you don't need to deal with.
160 |
161 | When another user downloads the same puzzle, I decrypt it with ``Foundation/Data/decrypted(key:)`` like so:
162 |
163 | ```swift
164 | func downloadPuzzle(from url: URL) async throws -> Puzzle {
165 | let encryptedData = // download logic
166 |
167 | let key = SymmetricKey(base64Encoded: "")!
168 | let plainData = try encryptedPuzzleData.decrypted(key: symmetricKey)
169 | return try JSONDecoder().decode(Puzzle.self, from: plainData)
170 | }
171 | ```
172 |
173 | > Note: HandySwift also conveniently ships with ``Swift/String/encrypted(key:)`` and ``Swift/String/decrypted(key:)`` functions for `String` which return a base-64 encoded String representation of the encrypted data. Use it when you're dealing with String APIs.
174 |
175 |
176 | ## Topics
177 |
178 | ### CaseIterable
179 |
180 | - ``Swift/CaseIterable/allCasesPrefixedByNil``
181 | - ``Swift/CaseIterable/allCasesSuffixedByNil``
182 |
183 | ### Collection
184 |
185 | - ``Swift/Collection/average()-3g44u``
186 | - ``Swift/Collection/average()-rtqg``
187 | - ``Swift/Collection/chunks(ofSize:)``
188 | - ``Swift/Collection/subscript(safe:)``
189 |
190 | ### Comparable
191 |
192 | - ``Swift/Comparable/clamped(to:)-5ky9b``
193 | - ``Swift/Comparable/clamped(to:)-4dzn7``
194 | - ``Swift/Comparable/clamped(to:)-8mqt8``
195 | - ``Swift/Comparable/clamp(to:)-4djv3``
196 | - ``Swift/Comparable/clamp(to:)-7bpgp``
197 | - ``Swift/Comparable/clamp(to:)-4ohw5``
198 |
199 | ### Data
200 |
201 | - ``Foundation/Data/encrypted(key:)``
202 | - ``Foundation/Data/decrypted(key:)``
203 |
204 | ### Date
205 |
206 | - ``Foundation/Date/reversed(by:)``
207 |
208 | ### Dictionary
209 |
210 | - ``Swift/Dictionary/init(keys:values:)``
211 | - ``Swift/Dictionary/mapKeys(_:)``
212 |
213 | ### DispatchTimeInterval
214 |
215 | - ``Dispatch/DispatchTimeInterval/timeInterval``
216 |
217 | ### Double
218 |
219 | - ``Swift/Double/round(fractionDigits:rule:)``
220 | - ``Swift/Double/rounded(fractionDigits:rule:)``
221 |
222 | ### Duration
223 |
224 | - ``Swift/Duration/timeInterval``
225 | - ``Swift/Duration/weeks(_:)``
226 | - ``Swift/Duration/days(_:)``
227 | - ``Swift/Duration/hours(_:)``
228 | - ``Swift/Duration/minutes(_:)``
229 | - ``Swift/Duration/autoscaleFormatted()``
230 | - ``Swift/Duration/multiplied(by:)-49cn7``
231 | - ``Swift/Duration/multiplied(by:)-6knjk``
232 | - ``Swift/Duration/divided(by:)-1h9df``
233 | - ``Swift/Duration/divided(by:)-2lwae``
234 | - ``Swift/Duration/divided(by:)-5s60j``
235 |
236 | ### Float
237 |
238 | - ``Swift/Float/round(fractionDigits:rule:)``
239 | - ``Swift/Float/rounded(fractionDigits:rule:)``
240 |
241 | ### Int
242 |
243 | - ``Swift/Int/times(_:)``
244 | - ``Swift/Int/timesMake(_:)``
245 |
246 | ### JSON Coding
247 |
248 | - ``Foundation/JSONDecoder/snakeCase``
249 | - ``Foundation/JSONEncoder/snakeCase``
250 |
251 | ### RandomAccessCollection
252 |
253 | - ``Swift/RandomAccessCollection/randomElements(count:)``
254 |
255 | ### Sequence
256 |
257 | - ``Swift/Sequence/sorted(byKeyPath:)``
258 | - ``Swift/Sequence/max(byKeyPath:)``
259 | - ``Swift/Sequence/min(byKeyPath:)``
260 | - ``Swift/Sequence/sum()``
261 | - ``Swift/Sequence/sum(mapToNumeric:)``
262 | - ``Swift/Sequence/count(where:)``
263 | - ``Swift/Sequence/count(where:equalTo:)``
264 | - ``Swift/Sequence/count(where:notEqualTo:)``
265 | - ``Swift/Sequence/count(where:prefixedBy:)``
266 | - ``Swift/Sequence/count(where:prefixedByOneOf:)``
267 | - ``Swift/Sequence/count(where:contains:)``
268 | - ``Swift/Sequence/count(where:containsOneOf:)``
269 | - ``Swift/Sequence/count(where:suffixedBy:)``
270 | - ``Swift/Sequence/count(where:suffixedByOneOf:)``
271 | - ``Swift/Sequence/count(where:greaterThan:)``
272 | - ``Swift/Sequence/count(where:greaterThanOrEqual:)``
273 | - ``Swift/Sequence/count(where:lessThan:)``
274 | - ``Swift/Sequence/count(where:lessThanOrEqual:)``
275 | - ``Swift/Sequence/filter(where:equalTo:)``
276 | - ``Swift/Sequence/filter(where:notEqualTo:)``
277 | - ``Swift/Sequence/filter(where:prefixedBy:)``
278 | - ``Swift/Sequence/filter(where:prefixedByOneOf:)``
279 | - ``Swift/Sequence/filter(where:contains:)``
280 | - ``Swift/Sequence/filter(where:containsOneOf:)``
281 | - ``Swift/Sequence/filter(where:suffixedBy:)``
282 | - ``Swift/Sequence/filter(where:suffixedByOneOf:)``
283 | - ``Swift/Sequence/filter(where:greaterThan:)``
284 | - ``Swift/Sequence/filter(where:greaterThanOrEqual:)``
285 | - ``Swift/Sequence/filter(where:lessThan:)``
286 | - ``Swift/Sequence/filter(where:lessThanOrEqual:)``
287 | - ``Swift/Sequence/first(where:equalTo:)``
288 | - ``Swift/Sequence/first(where:notEqualTo:)``
289 | - ``Swift/Sequence/first(where:prefixedBy:)``
290 | - ``Swift/Sequence/first(where:prefixedByOneOf:)``
291 | - ``Swift/Sequence/first(where:contains:)``
292 | - ``Swift/Sequence/first(where:containsOneOf:)``
293 | - ``Swift/Sequence/first(where:suffixedBy:)``
294 | - ``Swift/Sequence/first(where:suffixedByOneOf:)``
295 | - ``Swift/Sequence/first(where:greaterThan:)``
296 | - ``Swift/Sequence/first(where:greaterThanOrEqual:)``
297 | - ``Swift/Sequence/first(where:lessThan:)``
298 | - ``Swift/Sequence/first(where:lessThanOrEqual:)``
299 | - ``Swift/Sequence/count(prefixedBy:)``
300 | - ``Swift/Sequence/count(prefixedByOneOf:)``
301 | - ``Swift/Sequence/count(contains:)``
302 | - ``Swift/Sequence/count(containsOneOf:)``
303 | - ``Swift/Sequence/count(suffixedBy:)``
304 | - ``Swift/Sequence/count(suffixedByOneOf:)``
305 | - ``Swift/Sequence/count(greaterThan:)``
306 | - ``Swift/Sequence/count(greaterThanOrEqual:)``
307 | - ``Swift/Sequence/count(lessThan:)``
308 | - ``Swift/Sequence/count(lessThanOrEqual:)``
309 | - ``Swift/Sequence/filter(prefixedBy:)``
310 | - ``Swift/Sequence/filter(prefixedByOneOf:)``
311 | - ``Swift/Sequence/filter(contains:)``
312 | - ``Swift/Sequence/filter(containsOneOf:)``
313 | - ``Swift/Sequence/filter(suffixedBy:)``
314 | - ``Swift/Sequence/filter(suffixedByOneOf:)``
315 | - ``Swift/Sequence/filter(greaterThan:)``
316 | - ``Swift/Sequence/filter(greaterThanOrEqual:)``
317 | - ``Swift/Sequence/filter(lessThan:)``
318 | - ``Swift/Sequence/filter(lessThanOrEqual:)``
319 |
320 | ### String
321 |
322 | - ``Swift/String/isBlank``
323 | - ``Swift/String/fullRange``
324 | - ``Swift/String/fullNSRange``
325 | - ``Swift/String/init(randomWithLength:allowedCharactersType:)``
326 | - ``Swift/String/randomElements(count:)``
327 | - ``Swift/String/encrypted(key:)``
328 | - ``Swift/String/decrypted(key:)``
329 | - ``Swift/String/tokenized(locale:)``
330 | - ``Swift/String/matchesTokenizedPrefixes(in:locale:)``
331 |
332 | ### StringProtocol
333 |
334 | - ``Swift/StringProtocol/firstUppercased``
335 | - ``Swift/StringProtocol/firstLowercased``
336 |
337 | ### SymmetricKey
338 |
339 | - ``CryptoKit/SymmetricKey/base64EncodedString``
340 | - ``CryptoKit/SymmetricKey/init(base64Encoded:)``
341 |
342 | ### TimeInterval
343 |
344 | - ``Swift/Double/days``
345 | - ``Swift/Double/hours``
346 | - ``Swift/Double/minutes``
347 | - ``Swift/Double/seconds``
348 | - ``Swift/Double/milliseconds``
349 | - ``Swift/Double/microseconds``
350 | - ``Swift/Double/nanoseconds``
351 | - ``Swift/Double/days(_:)``
352 | - ``Swift/Double/hours(_:)``
353 | - ``Swift/Double/minutes(_:)``
354 | - ``Swift/Double/seconds(_:)``
355 | - ``Swift/Double/milliseconds(_:)``
356 | - ``Swift/Double/microseconds(_:)``
357 | - ``Swift/Double/nanoseconds(_:)``
358 | - ``Swift/Double/duration()``
359 |
360 |
361 | [TranslateKit]: https://apps.apple.com/app/apple-store/id6476773066?pt=549314&ct=swiftpackageindex.com&mt=8
362 | [CrossCraft]: https://apps.apple.com/app/apple-store/id6472669260?pt=549314&ct=swiftpackageindex.com&mt=8
363 | [FocusBeats]: https://apps.apple.com/app/apple-store/id6477829138?pt=549314&ct=swiftpackageindex.com&mt=8
364 | [Guided Guest Mode]: https://apps.apple.com/app/apple-store/id6479207869?pt=549314&ct=swiftpackageindex.com&mt=8
365 | [Posters]: https://apps.apple.com/app/apple-store/id6478062053?pt=549314&ct=swiftpackageindex.com&mt=8
366 |
--------------------------------------------------------------------------------
/Sources/HandySwift/HandySwift.docc/Essentials/New Types.md:
--------------------------------------------------------------------------------
1 | # New Types
2 |
3 | Adding missing types and global functions.
4 |
5 | @Metadata {
6 | @PageImage(purpose: icon, source: "HandySwift")
7 | @PageImage(purpose: card, source: "NewTypes")
8 | }
9 |
10 | ## Highlights
11 |
12 | In the [Topics](#topics) section below you can find a list of all new types & functions. Click on one to reveal more details.
13 |
14 | To get you started quickly, here are the ones I use in nearly all of my apps with a practical usage example for each:
15 |
16 | ### Gregorian Day & Time
17 |
18 | You want to construct a `Date` from year, month, and day? Easy:
19 |
20 | ```swift
21 | GregorianDay(year: 1960, month: 11, day: 01).startOfDay() // => Date
22 | ```
23 |
24 | You have a `Date` and want to store just the day part of the date, not the time? Just use ``GregorianDay`` in your model:
25 |
26 | ```swift
27 | struct User {
28 | let birthday: GregorianDay
29 | }
30 |
31 | let selectedDate = // coming from DatePicker
32 | let timCook = User(birthday: GregorianDay(date: selectedDate))
33 | print(timCook.birthday.iso8601Formatted) // => "1960-11-01"
34 | ```
35 |
36 | You just want today's date without time?
37 |
38 | ```swift
39 | GregorianDay.today
40 | ```
41 |
42 | Works also with `.yesterday` and `.tomorrow`. For more, just call:
43 |
44 | ```swift
45 | let todayNextWeek = GregorianDay.today.advanced(by: 7)
46 | ```
47 |
48 | > Note: `GregorianDay` conforms to all the protocols you would expect, such as `Codable`, `Hashable`, and `Comparable`. For encoding/decoding, it uses the ISO format as in "2014-07-13".
49 |
50 | ``GregorianTime`` is the counterpart:
51 |
52 | ```swift
53 | let iPhoneAnnounceTime = GregorianTime(hour: 09, minute: 41)
54 | let anHourFromNow = GregorianTime.now.advanced(by: .hours(1))
55 |
56 | let date = iPhoneAnnounceTime.date(day: GregorianDay.today) // => Date
57 | ```
58 |
59 | ### Delay & Debounce
60 |
61 | Have you ever wanted to delay some code and found this API annoying to remember & type out?
62 |
63 | ```swift
64 | DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .milliseconds(250)) {
65 | // your code
66 | }
67 | ```
68 |
69 | HandySwift introduces a shorter version that's easier to remember:
70 |
71 | ```swift
72 | delay(by: .milliseconds(250)) {
73 | // your code
74 | }
75 | ```
76 |
77 | It also supports different Quality of Service classes like `DispatchQueue` (default is main queue):
78 |
79 | ```swift
80 | delay(by: .milliseconds(250), qosClass: .background) {
81 | // your code
82 | }
83 | ```
84 |
85 | While delaying is great for one-off tasks, sometimes there's fast input that causes performance or scalability issues. For example, a user might type fast in a search field. It's common practice to delay updating the search results and additionally cancelling any older inputs once the user makes a new one. This practice is called "Debouncing". And it's easy with HandySwift:
86 |
87 | ```swift
88 | @State private var searchText = ""
89 | let debouncer = Debouncer()
90 |
91 | var body: some View {
92 | List(filteredItems) { item in
93 | Text(item.title)
94 | }
95 | .searchable(text: self.$searchText)
96 | .onChange(of: self.searchText) { newValue in
97 | self.debouncer.delay(for: .milliseconds(500)) {
98 | // Perform search operation with the updated search text after 500 milliseconds of user inactivity
99 | self.performSearch(with: newValue)
100 | }
101 | }
102 | .onDisappear {
103 | debouncer.cancelAll()
104 | }
105 | }
106 | ```
107 |
108 | Note that the ``Debouncer`` was stored in a property so ``Debouncer/cancelAll()`` could be called on disappear for cleanup. But the ``Debouncer/delay(for:id:operation:)-83bbm`` is where the magic happens – and you don't have to deal with the details!
109 |
110 | > Note: If you need multiple debouncing operations in one view, you don't need multiple debouncers. Just pass an `id` to the delay function.
111 |
112 |
113 | ## Topics
114 |
115 | ### Collections
116 |
117 | - ``FrequencyTable``
118 | - ``SortedArray``
119 |
120 | ### Date & Time
121 |
122 | - ``GregorianDay``
123 | - ``GregorianTime``
124 |
125 | ### UI Helpers
126 |
127 | - ``Debouncer``
128 | - ``OperatingSystem`` (short: ``OS``)
129 |
130 | ### Other
131 |
132 | - ``delay(by:qosClass:_:)-8iw4f``
133 | - ``delay(by:qosClass:_:)-yedf``
134 | - ``HandyRegex``
135 |
--------------------------------------------------------------------------------
/Sources/HandySwift/HandySwift.docc/HandySwift.md:
--------------------------------------------------------------------------------
1 | # ``HandySwift``
2 |
3 | Handy Swift features that didn't make it into the Swift standard library.
4 |
5 | @Metadata {
6 | @PageImage(purpose: icon, source: "HandySwift")
7 | }
8 |
9 | ## Essentials
10 |
11 | Learn how you can make the most of HandySwift with these guides:
12 |
13 | @Links(visualStyle: detailedGrid) {
14 | -
15 | -
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/HandySwift/HandySwift.docc/Resources/Extensions.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlineDev/HandySwift/e9defa22f6c0f3ae529b6256375d679061f830ee/Sources/HandySwift/HandySwift.docc/Resources/Extensions.jpeg
--------------------------------------------------------------------------------
/Sources/HandySwift/HandySwift.docc/Resources/Extensions/APIKeys.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlineDev/HandySwift/e9defa22f6c0f3ae529b6256375d679061f830ee/Sources/HandySwift/HandySwift.docc/Resources/Extensions/APIKeys.png
--------------------------------------------------------------------------------
/Sources/HandySwift/HandySwift.docc/Resources/Extensions/CrosswordGeneration.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlineDev/HandySwift/e9defa22f6c0f3ae529b6256375d679061f830ee/Sources/HandySwift/HandySwift.docc/Resources/Extensions/CrosswordGeneration.png
--------------------------------------------------------------------------------
/Sources/HandySwift/HandySwift.docc/Resources/Extensions/MusicPlayer.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlineDev/HandySwift/e9defa22f6c0f3ae529b6256375d679061f830ee/Sources/HandySwift/HandySwift.docc/Resources/Extensions/MusicPlayer.jpeg
--------------------------------------------------------------------------------
/Sources/HandySwift/HandySwift.docc/Resources/Extensions/PremiumPlanExpires.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlineDev/HandySwift/e9defa22f6c0f3ae529b6256375d679061f830ee/Sources/HandySwift/HandySwift.docc/Resources/Extensions/PremiumPlanExpires.png
--------------------------------------------------------------------------------
/Sources/HandySwift/HandySwift.docc/Resources/Extensions/ProgressBar.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlineDev/HandySwift/e9defa22f6c0f3ae529b6256375d679061f830ee/Sources/HandySwift/HandySwift.docc/Resources/Extensions/ProgressBar.jpeg
--------------------------------------------------------------------------------
/Sources/HandySwift/HandySwift.docc/Resources/Extensions/SharePuzzle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlineDev/HandySwift/e9defa22f6c0f3ae529b6256375d679061f830ee/Sources/HandySwift/HandySwift.docc/Resources/Extensions/SharePuzzle.png
--------------------------------------------------------------------------------
/Sources/HandySwift/HandySwift.docc/Resources/HandySwift.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlineDev/HandySwift/e9defa22f6c0f3ae529b6256375d679061f830ee/Sources/HandySwift/HandySwift.docc/Resources/HandySwift.png
--------------------------------------------------------------------------------
/Sources/HandySwift/HandySwift.docc/Resources/NewTypes.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlineDev/HandySwift/e9defa22f6c0f3ae529b6256375d679061f830ee/Sources/HandySwift/HandySwift.docc/Resources/NewTypes.jpeg
--------------------------------------------------------------------------------
/Sources/HandySwift/HandySwift.docc/theme-settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "theme": {
3 | "color": {
4 | "header": "#002B7D",
5 | "documentation-intro-title": "#FFFFFF",
6 | "documentation-intro-figure": "#FFFFFF",
7 | "documentation-intro-fill": "radial-gradient(circle at top, var(--color-header) 30%, #000 100%)",
8 | "documentation-intro-accent": "var(--color-header)"
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Sources/HandySwift/Protocols/AutoConforming.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// Adds conformance to the automatically inferrable protocols `Hashable`, `Codable`, and `Sendable`.
4 | public typealias AutoConforming = Hashable & Codable & Sendable
5 |
--------------------------------------------------------------------------------
/Sources/HandySwift/Protocols/DivisibleArithmetic.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// A type that conforms to `DivisibleArithmetic` provides basic arithmetic operations: addition, subtraction, multiplication, and division.
4 | public protocol DivisibleArithmetic: Numeric {
5 | /// Initializes an instance with the given integer value.
6 | ///
7 | /// - Parameter value: An integer value to initialize the instance.
8 | init(_ value: Int)
9 |
10 | /// Divides one value by another and returns the result.
11 | ///
12 | /// - Parameters:
13 | /// - lhs: The dividend.
14 | /// - rhs: The divisor.
15 | /// - Returns: The quotient of dividing `lhs` by `rhs`.
16 | static func / (lhs: Self, rhs: Self) -> Self
17 | }
18 |
19 | extension Double: DivisibleArithmetic {}
20 | extension Float: DivisibleArithmetic {}
21 |
22 | #if canImport(CoreGraphics)
23 | import CoreGraphics
24 |
25 | extension CGFloat: DivisibleArithmetic {}
26 | #endif
27 |
--------------------------------------------------------------------------------
/Sources/HandySwift/Protocols/Withable.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// Simple protocol to facilitate modifying objects with multiple properties in a chainable and functional manner.
4 | public protocol Withable { /* no requirements */ }
5 |
6 | extension Withable {
7 | /// Returns a copy of the object (if a struct) or uses the same object (if a class), with modifications applied in a chainable manner.
8 | /// This is particularly useful for configuring objects upon initialization or making successive modifications.
9 | ///
10 | /// Example:
11 | /// ```swift
12 | /// struct Foo: Withable {
13 | /// var bar: Int
14 | /// var isEasy: Bool = false
15 | /// }
16 | ///
17 | /// let defaultFoo = Foo(bar: 5)
18 | /// let customFoo = Foo(bar: 5).with {
19 | /// $0.isEasy = true
20 | /// }
21 | ///
22 | /// print(defaultFoo.isEasy) // false
23 | /// print(customFoo.isEasy) // true
24 | /// ```
25 | ///
26 | /// - Parameter config: A closure that accepts an inout reference to the object and modifies its properties.
27 | /// - Returns: A modified copy of the object (if a struct) or the same object (if a class).
28 | @inlinable
29 | public func with(_ config: (inout Self) throws -> Void) rethrows -> Self {
30 | var copy = self
31 | try config(©)
32 | return copy
33 | }
34 | }
35 |
36 | #if !os(Linux)
37 | extension NSObject: Withable {}
38 | #endif
39 |
--------------------------------------------------------------------------------
/Sources/HandySwift/Types/Debouncer.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// A class for debouncing operations.
4 | ///
5 | /// Debouncing ensures that an operation is not executed multiple times within a given time frame, cancelling any duplicate operations.
6 | /// Only the last operation will be executed, and only after the given time frame has passed.
7 | ///
8 | /// - Note: This is useful for improving user experience in scenarios like search functionalities in apps, where you might want to reduce the number of search operations triggered by keystroke events.
9 | ///
10 | /// Example for SwiftUI's `.searchable` modifier:
11 | /// ```swift
12 | /// @State private var searchText = ""
13 | /// let debouncer = Debouncer()
14 | ///
15 | /// var body: some View {
16 | /// List(filteredItems) { item in
17 | /// Text(item.title)
18 | /// }
19 | /// .searchable(text: self.$searchText)
20 | /// .onChange(of: self.searchText) { newValue in
21 | /// self.debouncer.delay(for: 0.5) {
22 | /// // Perform search operation with the updated search text after 500 milliseconds of user inactivity
23 | /// self.performSearch(with: newValue)
24 | /// }
25 | /// }
26 | /// .onDisappear {
27 | /// debouncer.cancelAll()
28 | /// }
29 | /// }
30 | /// ```
31 | public final class Debouncer {
32 | private var timerByID: [String: Timer] = [:]
33 |
34 | /// Initializes a new instance of the `Debouncer` class.
35 | public init() {}
36 |
37 | /// Delays operations and cancels all but the last one when the same `id` is provided.
38 | ///
39 | /// - Parameters:
40 | /// - duration: The time duration for the delay.
41 | /// - id: An optional identifier to distinguish different delays (default is "default").
42 | /// - operation: The operation to be delayed and executed.
43 | ///
44 | /// This version of `delay` uses a `Duration` to specify the delay time, available in iOS 16 and later.
45 | ///
46 | /// Example:
47 | /// ```swift
48 | /// let debouncer = Debouncer()
49 | /// debouncer.delay(for: .milliseconds(500), id: "search") {
50 | /// // Perform some operation after a 500 milliseconds delay
51 | /// performOperation()
52 | /// }
53 | /// ```
54 | @available(iOS 16, macOS 13, tvOS 16, visionOS 1, watchOS 9, *)
55 | public func delay(for duration: Duration, id: String = "default", operation: @Sendable @escaping () -> Void) {
56 | self.cancel(id: id)
57 | self.timerByID[id] = Timer.scheduledTimer(withTimeInterval: duration.timeInterval, repeats: false) { _ in
58 | operation()
59 | }
60 | }
61 |
62 | /// Delays operations and cancels all but the last one when the same `id` is provided.
63 | ///
64 | /// - Parameters:
65 | /// - interval: The time interval for the delay.
66 | /// - id: An optional identifier to distinguish different delays (default is "default").
67 | /// - operation: The operation to be delayed and executed.
68 | ///
69 | /// This version of `delay` uses a `TimeInterval` to specify the delay time.
70 | ///
71 | /// Example for a generic operation:
72 | /// ```swift
73 | /// let debouncer = Debouncer()
74 | /// debouncer.delay(for: 0.5, id: "genericOperation") {
75 | /// // Perform some operation after a 500 milliseconds delay
76 | /// performOperation()
77 | /// }
78 | /// ```
79 | public func delay(for interval: TimeInterval, id: String = "default", operation: @Sendable @escaping () -> Void) {
80 | self.cancel(id: id)
81 | self.timerByID[id] = Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { _ in
82 | operation()
83 | }
84 | }
85 |
86 | /// Cancels any in-flight operations with the provided `id`.
87 | ///
88 | /// - Parameter id: The identifier of the operation to be canceled.
89 | ///
90 | /// Example:
91 | /// ```swift
92 | /// var body: some View {
93 | /// ContentView()
94 | /// .onDisappear {
95 | /// debouncer.cancel(id: "search")
96 | /// }
97 | /// }
98 | /// ```
99 | public func cancel(id: String) {
100 | self.timerByID[id]?.invalidate()
101 | }
102 |
103 | /// Cancels all in-flight operations independent of their `id`. This could be called `.onDisappear` when this is used in a SwiftUI view, for example.
104 | ///
105 | /// Example:
106 | /// ```swift
107 | /// var body: some View {
108 | /// ContentView()
109 | /// .onDisappear {
110 | /// debouncer.cancelAll()
111 | /// }
112 | /// }
113 | /// ```
114 | public func cancelAll() {
115 | for timer in self.timerByID.values {
116 | timer.invalidate()
117 | }
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/Sources/HandySwift/Types/FrequencyTable.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// Data structure to retrieve random values with their frequency taken into account.
4 | ///
5 | /// Example:
6 | /// ```swift
7 | /// let fruits = ["apple", "banana", "orange"]
8 | /// let frequencyTable = FrequencyTable(values: fruits) { fruit in
9 | /// switch fruit {
10 | /// case "apple": 5
11 | /// case "banana": 3
12 | /// case "orange": 2
13 | /// default: 0
14 | /// }
15 | /// }
16 | ///
17 | /// let randomFruit = frequencyTable.randomElement()
18 | /// print(randomFruit) // Prints a random fruit considering the specified frequencies.
19 | /// ```
20 | public struct FrequencyTable {
21 | @usableFromInline
22 | typealias Entry = (value: T, frequency: Int)
23 |
24 | @usableFromInline
25 | internal let valuesWithFrequencies: [Entry]
26 |
27 | /// Contains all values the amount of time of their frequencies.
28 | @usableFromInline
29 | internal let frequentValues: [T]
30 |
31 | /// Creates a new FrequencyTable instance with values and their frequencies provided.
32 | ///
33 | /// - Parameters:
34 | /// - values: An array full of values to be saved into the frequency table.
35 | /// - frequencyClosure: The closure to specify the frequency for a specific value.
36 | /// - Throws: Any errors thrown by the `frequencyClosure` are propagated upward.
37 | ///
38 | /// Example:
39 | /// ```swift
40 | /// let fruits = ["apple", "banana", "orange"]
41 | /// let frequencyTable = FrequencyTable(values: fruits) { fruit in
42 | /// switch fruit {
43 | /// case "apple": 5
44 | /// case "banana": 3
45 | /// case "orange": 2
46 | /// default: 0
47 | /// }
48 | /// }
49 | /// ```
50 | @inlinable
51 | public init(values: [T], frequencyClosure: (T) throws -> Int) rethrows {
52 | valuesWithFrequencies = try values.map { ($0, try frequencyClosure($0)) }
53 | frequentValues = valuesWithFrequencies.reduce(into: []) { memo, entry in
54 | memo += Array(repeating: entry.value, count: entry.frequency)
55 | }
56 | }
57 |
58 | /// Returns a random value taking frequencies into account or nil if values are empty.
59 | ///
60 | /// - Returns: A random value taking frequencies into account or nil if values are empty.
61 | ///
62 | /// Example:
63 | /// ```swift
64 | /// let fruits = ["apple", "banana", "orange"]
65 | /// let frequencyTable = try FrequencyTable(values: fruits) { fruit in
66 | /// switch fruit {
67 | /// case "apple": 5
68 | /// case "banana": 3
69 | /// case "orange": 2
70 | /// default: 0
71 | /// }
72 | /// }
73 | ///
74 | /// let randomFruit = frequencyTable.randomElement()
75 | /// print(randomFruit) // Prints a random fruit considering the specified frequencies.
76 | /// ```
77 | @inlinable
78 | public func randomElement() -> T? { self.frequentValues.randomElement() }
79 |
80 | /// Returns an array of random values taking frequencies into account or nil if values are empty.
81 | ///
82 | /// - Parameters:
83 | /// - count: The size of the resulting array of random values.
84 | /// - Returns: An array of random values or nil if values are empty.
85 | ///
86 | /// Example:
87 | /// ```swift
88 | /// let fruits = ["apple", "banana", "orange"]
89 | /// let frequencyTable = try FrequencyTable(values: fruits) { fruit in
90 | /// switch fruit {
91 | /// case "apple": 5
92 | /// case "banana": 3
93 | /// case "orange": 2
94 | /// default: 0
95 | /// }
96 | /// }
97 | ///
98 | /// let randomFruits = frequencyTable.randomElements(count: 3)
99 | /// print(randomFruits) // Prints an array of random fruits considering the specified frequencies.
100 | /// ```
101 | @inlinable
102 | public func randomElements(count: Int) -> [T]? {
103 | guard !self.frequentValues.isEmpty else { return nil }
104 | return count.timesMake { self.frequentValues.randomElement()! }
105 | }
106 | }
107 |
108 | // - MARK: Migration
109 | extension FrequencyTable {
110 | @available(*, unavailable, renamed: "randomElement()")
111 | public var sample: T? { fatalError() }
112 |
113 | @available(*, unavailable, renamed: "randomElements(count:)")
114 | public func sample(size: Int) -> [T]? { fatalError() }
115 | }
116 |
--------------------------------------------------------------------------------
/Sources/HandySwift/Types/GregorianDay.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// A date without time information.
4 | ///
5 | /// Example:
6 | /// ```swift
7 | /// let yesterday = GregorianDay.yesterday
8 | /// print(yesterday.iso8601Formatted) // Prints the current date in ISO 8601 format, e.g. "2024-03-20"
9 | ///
10 | /// let tomorrow = yesterday.advanced(by: 2)
11 | /// let timCookBirthday = GregorianDay(year: 1960, month: 11, day: 01)
12 | ///
13 | /// let startOfDay = GregorianDay(date: Date()).startOfDay()
14 | /// ```
15 | public struct GregorianDay {
16 | /// The year component of the date.
17 | public var year: Int
18 | /// The month component of the date.
19 | public var month: Int
20 | /// The day component of the date.
21 | public var day: Int
22 |
23 | /// Returns an ISO 8601 formatted String representation of the date, e.g., `2024-02-24`.
24 | public var iso8601Formatted: String {
25 | "\(String(format: "%04d", self.year))-\(String(format: "%02d", self.month))-\(String(format: "%02d", self.day))"
26 | }
27 |
28 | /// Initializes a `GregorianDay` instance with the given `Date`.
29 | ///
30 | /// - Parameter date: The date to extract components from.
31 | public init(date: Date) {
32 | let components = Calendar(identifier: .gregorian).dateComponents([.year, .month, .day], from: date)
33 | self.year = components.year!
34 | self.month = components.month!
35 | self.day = components.day!
36 | }
37 |
38 | /// Initializes a `GregorianDay` instance with the given year, month, and day.
39 | ///
40 | /// - Parameters:
41 | /// - year: The year of the date.
42 | /// - month: The month of the date.
43 | /// - day: The day of the date.
44 | public init(year: Int, month: Int, day: Int) {
45 | assert(month >= 1 && month <= 12)
46 | assert(day >= 1 && day <= 31)
47 |
48 | self.year = year
49 | self.month = month
50 | self.day = day
51 | }
52 |
53 | /// Advances the date by the specified number of days.
54 | ///
55 | /// - Parameter days: The number of days to advance the date by.
56 | /// - Returns: A new `GregorianDay` instance advanced by the specified number of days.
57 | ///
58 | /// Example:
59 | /// ```swift
60 | /// let tomorrow = GregorianDay.today.advanced(by: 1)
61 | /// ```
62 | public func advanced(by days: Int) -> Self {
63 | GregorianDay(date: self.midOfDay().addingTimeInterval(.days(Double(days))))
64 | }
65 |
66 | /// Reverses the date by the specified number of days.
67 | ///
68 | /// - Parameter days: The number of days to reverse the date by.
69 | /// - Returns: A new `GregorianDay` instance reversed by the specified number of days.
70 | ///
71 | /// Example:
72 | /// ```swift
73 | /// let yesterday = GregorianDay.today.reversed(by: 1)
74 | /// ```
75 | public func reversed(by days: Int) -> Self {
76 | self.advanced(by: -days)
77 | }
78 |
79 | /// Advances the date by the specified number of months.
80 | ///
81 | /// - Parameter months: The number of months to advance the date by.
82 | /// - Returns: A new `GregorianDay` instance advanced by the specified number of months.
83 | ///
84 | /// - Warning: This may return an invalid date such as February 31st. Only use in combination with a method like ``startOfMonth(timeZone:)`` that removes the day.
85 | ///
86 | /// Example:
87 | /// ```swift
88 | /// let tomorrow = GregorianDay.today.advanced(byMonths: 1)
89 | /// ```
90 | public func advanced(byMonths months: Int) -> Self {
91 | let (overflowingYears, newMonth) = (self.month + months - 1).quotientAndRemainder(dividingBy: 12)
92 | return self.with { $0.year += overflowingYears; $0.month = newMonth + 1 }
93 | }
94 |
95 | /// Reverses the date by the specified number of months.
96 | ///
97 | /// - Parameter months: The number of months to reverse the date by.
98 | /// - Returns: A new `GregorianDay` instance reversed by the specified number of months.
99 | ///
100 | /// - Warning: This may return an invalid date such as February 31st. Only use in combination with a method like ``startOfMonth(timeZone:)`` that removes the day.
101 | ///
102 | /// Example:
103 | /// ```swift
104 | /// let yesterday = GregorianDay.today.reversed(byMonths: 1)
105 | /// ```
106 | public func reversed(byMonths months: Int) -> Self {
107 | self.advanced(byMonths: -months)
108 | }
109 |
110 | /// Advances the date by the specified number of years.
111 | ///
112 | /// - Parameter years: The number of years to advance the date by.
113 | /// - Returns: A new `GregorianDay` instance advanced by the specified number of years. The day and month stay the same.
114 | ///
115 | /// - Warning: This may return an invalid date such as February 31st. Only use in combination with a method like ``startOfMonth(timeZone:)`` that removes the day.
116 | ///
117 | /// Example:
118 | /// ```swift
119 | /// let tomorrow = GregorianDay.today.advanced(byYears: 1)
120 | /// ```
121 | public func advanced(byYears years: Int) -> Self {
122 | self.with { $0.year += years }
123 | }
124 |
125 | /// Reverses the date by the specified number of years.
126 | ///
127 | /// - Parameter years: The number of years to reverse the date by.
128 | /// - Returns: A new `GregorianDay` instance reversed by the specified number of years. The day and month stay the same.
129 | ///
130 | /// - Warning: This may return an invalid date such as February 31st. Only use in combination with a method like ``startOfMonth(timeZone:)`` that removes the day.
131 | /// Example:
132 | /// ```swift
133 | /// let yesterday = GregorianDay.today.reversed(byYears: 1)
134 | /// ```
135 | public func reversed(byYears years: Int) -> Self {
136 | self.advanced(byYears: -years)
137 | }
138 |
139 | /// Returns the start of the day represented by the date.
140 | ///
141 | /// - Parameter timeZone: The time zone for which to calculate the start of the day. Defaults to the users current timezone.
142 | /// - Returns: A `Date` representing the start of the day.
143 | ///
144 | /// Example:
145 | /// ```swift
146 | /// let startOfToday = GregorianDay.today.startOfDay()
147 | /// ```
148 | public func startOfDay(timeZone: TimeZone = .current) -> Date {
149 | let components = DateComponents(
150 | calendar: Calendar(identifier: .gregorian),
151 | timeZone: timeZone,
152 | year: self.year,
153 | month: self.month,
154 | day: self.day
155 | )
156 | return components.date!
157 | }
158 |
159 | /// Returns the start of the month represented by the date.
160 | ///
161 | /// - Parameter timeZone: The time zone for which to calculate the start of the month. Defaults to the users current timezone.
162 | /// - Returns: A `Date` representing the start of the month.
163 | ///
164 | /// Example:
165 | /// ```swift
166 | /// let startOfThisMonth = GregorianDay.today.startOfMonth()
167 | /// ```
168 | public func startOfMonth(timeZone: TimeZone = .current) -> Date {
169 | let components = DateComponents(
170 | calendar: Calendar(identifier: .gregorian),
171 | timeZone: timeZone,
172 | year: self.year,
173 | month: self.month,
174 | day: 1
175 | )
176 | return components.date!
177 | }
178 |
179 | /// Returns the start of the year represented by the date.
180 | ///
181 | /// - Parameter timeZone: The time zone for which to calculate the start of the year. Defaults to the users current timezone.
182 | /// - Returns: A `Date` representing the start of the year.
183 | ///
184 | /// Example:
185 | /// ```swift
186 | /// let startOfThisYear = GregorianDay.today.startOfYear()
187 | /// ```
188 | public func startOfYear(timeZone: TimeZone = .current) -> Date {
189 | let components = DateComponents(
190 | calendar: Calendar(identifier: .gregorian),
191 | timeZone: timeZone,
192 | year: self.year,
193 | month: 1,
194 | day: 1
195 | )
196 | return components.date!
197 | }
198 |
199 | /// Returns the middle of the day represented by the date.
200 | ///
201 | /// - Parameter timeZone: The time zone for which to calculate the middle of the day. Defaults to UTC.
202 | /// - Returns: A `Date` representing the middle of the day.
203 | ///
204 | /// - Note: If you need to pass a `Date` to an API that only cares about the day (not the time), calling ``midOfDay(timeZone:)`` ensures you get the same day independent of timezones.
205 | ///
206 | /// Example:
207 | /// ```swift
208 | /// let midOfToday: Date = GregorianDay.today.midOfDay() // the middle of today in UTC time zone
209 | /// ```
210 | public func midOfDay(timeZone: TimeZone = TimeZone(secondsFromGMT: 0)!) -> Date {
211 | let components = DateComponents(
212 | calendar: Calendar(identifier: .gregorian),
213 | timeZone: timeZone,
214 | year: self.year,
215 | month: self.month,
216 | day: self.day,
217 | hour: 12
218 | )
219 | return components.date!
220 | }
221 |
222 | /// Returns a `Date` representing this day at the specified time.
223 | ///
224 | /// - Parameters:
225 | /// - timeOfDay: The time of day to set for the resulting date.
226 | /// - timeZone: The time zone for which to calculate the date. Defaults to the users current timezone.
227 | /// - Returns: A `Date` representing this day at the specified time.
228 | ///
229 | /// Example:
230 | /// ```swift
231 | /// let noonToday = GregorianDay.today.date(timeOfDay: .noon) // today at 12:00
232 | /// ```
233 | public func date(timeOfDay: GregorianTime, timeZone: TimeZone = .current) -> Date {
234 | timeOfDay.date(day: self, timeZone: timeZone)
235 | }
236 | }
237 |
238 | extension GregorianDay: Codable {
239 | /// Initializes a `GregorianDay` instance by decoding from the provided decoder.
240 | ///
241 | /// - Parameter decoder: The decoder to read data from.
242 | /// - Throws: An error if reading from the decoder fails, or if the data is corrupted or cannot be decoded.
243 | public init(from decoder: Decoder) throws {
244 | let container = try decoder.singleValueContainer()
245 | let dateString = try container.decode(String.self)
246 |
247 | let formatter = DateFormatter()
248 | formatter.dateFormat = "yyyy-MM-dd"
249 | formatter.calendar = Calendar(identifier: .gregorian)
250 |
251 | guard let date = formatter.date(from: dateString) else {
252 | throw DecodingError.dataCorruptedError(in: container, debugDescription: "Cannot decode date string")
253 | }
254 |
255 | self = GregorianDay(date: date)
256 | }
257 |
258 | /// Encodes the `GregorianDay` instance into the provided encoder.
259 | ///
260 | /// - Parameter encoder: The encoder to write data to.
261 | /// - Throws: An error if encoding fails.
262 | public func encode(to encoder: Encoder) throws {
263 | var container = encoder.singleValueContainer()
264 | try container.encode(self.iso8601Formatted)
265 | }
266 | }
267 |
268 | extension GregorianDay: Hashable, Sendable {}
269 | extension GregorianDay: Identifiable {
270 | /// The identifier of the `GregorianDay` instance, which is a string representation of its year, month, and day.
271 | public var id: String { "\(self.year)-\(self.month)-\(self.day)" }
272 | }
273 |
274 | extension GregorianDay: Comparable {
275 | /// Compares two `GregorianDay` instances for order.
276 | ///
277 | /// - Parameters:
278 | /// - left: The first `GregorianDay` instance to compare.
279 | /// - right: The second `GregorianDay` instance to compare.
280 | /// - Returns: `true` if the `left` date is less than the `right` date; otherwise, `false`.
281 | public static func < (left: GregorianDay, right: GregorianDay) -> Bool {
282 | guard left.year == right.year else { return left.year < right.year }
283 | guard left.month == right.month else { return left.month < right.month }
284 | return left.day < right.day
285 | }
286 | }
287 |
288 | extension GregorianDay {
289 | /// The `GregorianDay` representing yesterday's date.
290 | public static var yesterday: Self { GregorianDay(date: Date()).advanced(by: -1) }
291 |
292 | /// The `GregorianDay` representing today's date.
293 | public static var today: Self { GregorianDay(date: Date()) }
294 |
295 | /// The `GregorianDay` representing tomorrow's date.
296 | public static var tomorrow: Self { GregorianDay(date: Date()).advanced(by: 1) }
297 | }
298 |
299 | extension GregorianDay: Withable {}
300 |
--------------------------------------------------------------------------------
/Sources/HandySwift/Types/GregorianTime.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// A time without date info.
4 | ///
5 | /// `GregorianTime` represents a time of day without any associated date information. It provides functionalities to work with time components like hour, minute, and second, and perform operations such as initializing from a given date, calculating durations, advancing, and reversing time.
6 | ///
7 | /// Example:
8 | /// ```swift
9 | /// // Initializing from a given date
10 | /// let date = Date()
11 | /// let timeOfDay = GregorianTime(date: date)
12 | ///
13 | /// // Calculating duration since the start of the day
14 | /// let durationSinceStartOfDay: Duration = timeOfDay.durationSinceStartOfDay
15 | /// let timeIntervalSinceStartOfDay: TimeInterval = durationSinceStartOfDay.timeInterval
16 | ///
17 | /// // Advancing time by a duration
18 | /// let advancedTime = timeOfDay.advanced(by: .hours(2) + .minutes(30))
19 | ///
20 | /// // Reversing time by a duration
21 | /// let reversedTime = timeOfDay.reversed(by: .minutes(15))
22 | /// ```
23 | public struct GregorianTime {
24 | /// The number of days beyond the current day.
25 | public var overflowingDays: Int
26 | /// The hour component of the time.
27 | public var hour: Int
28 | /// The minute component of the time.
29 | public var minute: Int
30 | /// The second component of the time.
31 | public var second: Int
32 |
33 | /// Initializes a `GregorianTime` instance from a given date.
34 | ///
35 | /// - Parameter date: The date from which to extract time components.
36 | public init(date: Date) {
37 | let components = Calendar(identifier: .gregorian).dateComponents([.hour, .minute, .second], from: date)
38 | self.overflowingDays = 0
39 | self.hour = components.hour!
40 | self.minute = components.minute!
41 | self.second = components.second!
42 | }
43 |
44 | /// Initializes a `GregorianTime` instance with the provided time components.
45 | ///
46 | /// - Parameters:
47 | /// - hour: The hour component.
48 | /// - minute: The minute component.
49 | /// - second: The second component (default is 0).
50 | public init(hour: Int, minute: Int, second: Int = 0) {
51 | assert(hour >= 0 && hour < 24)
52 | assert(minute >= 0 && minute < 60)
53 | assert(second >= 0 && second < 60)
54 |
55 | self.overflowingDays = 0
56 | self.hour = hour
57 | self.minute = minute
58 | self.second = second
59 | }
60 |
61 | /// Returns a `Date` object representing the time on a given day.
62 | ///
63 | /// - Parameters:
64 | /// - day: The day to which the time belongs.
65 | /// - timeZone: The time zone to use for the conversion (default is the current time zone).
66 | /// - Returns: A `Date` object representing the time.
67 | public func date(day: GregorianDay, timeZone: TimeZone = .current) -> Date {
68 | let components = DateComponents(
69 | calendar: Calendar(identifier: .gregorian),
70 | timeZone: timeZone,
71 | year: day.year,
72 | month: day.month,
73 | day: day.day,
74 | hour: self.hour,
75 | minute: self.minute,
76 | second: self.second
77 | )
78 | return components.date!.addingTimeInterval(.days(Double(self.overflowingDays)))
79 | }
80 |
81 | /// Initializes a `GregorianTime` instance from the duration since the start of the day.
82 | ///
83 | /// - Parameter durationSinceStartOfDay: The duration since the start of the day.
84 | @available(iOS 16, macOS 13, tvOS 16, visionOS 1, watchOS 9, *)
85 | public init(durationSinceStartOfDay: Duration) {
86 | self.overflowingDays = Int(durationSinceStartOfDay.timeInterval.days)
87 | self.hour = Int((durationSinceStartOfDay - .days(self.overflowingDays)).timeInterval.hours)
88 | self.minute = Int((durationSinceStartOfDay - .days(self.overflowingDays) - .hours(self.hour)).timeInterval.minutes)
89 | self.second = Int((durationSinceStartOfDay - .days(self.overflowingDays) - .hours(self.hour) - .minutes(self.minute)).timeInterval.seconds)
90 | }
91 |
92 | /// Returns the duration since the start of the day.
93 | @available(iOS 16, macOS 13, tvOS 16, visionOS 1, watchOS 9, *)
94 | public var durationSinceStartOfDay: Duration {
95 | .days(self.overflowingDays) + .hours(self.hour) + .minutes(self.minute) + .seconds(self.second)
96 | }
97 |
98 | /// Advances the time by the specified duration.
99 | ///
100 | /// - Parameter duration: The duration by which to advance the time.
101 | /// - Returns: A new `GregorianTime` instance advanced by the specified duration.
102 | @available(iOS 16, macOS 13, tvOS 16, visionOS 1, watchOS 9, *)
103 | public func advanced(by duration: Duration) -> Self {
104 | GregorianTime(durationSinceStartOfDay: self.durationSinceStartOfDay + duration)
105 | }
106 |
107 | /// Reverses the time by the specified duration.
108 | ///
109 | /// - Parameter duration: The duration by which to reverse the time.
110 | /// - Returns: A new `GregorianTime` instance reversed by the specified duration.
111 | @available(iOS 16, macOS 13, tvOS 16, visionOS 1, watchOS 9, *)
112 | public func reversed(by duration: Duration) -> Self {
113 | GregorianTime(durationSinceStartOfDay: self.durationSinceStartOfDay - duration)
114 | }
115 | }
116 |
117 | extension GregorianTime: Codable, Hashable, Sendable {}
118 | extension GregorianTime: Identifiable {
119 | /// The unique identifier of the time, formatted as "hour:minute:second".
120 | public var id: String { "\(self.hour):\(self.minute):\(self.second)" }
121 | }
122 |
123 | extension GregorianTime: Comparable {
124 | /// Compares two `GregorianTime` instances.
125 | ///
126 | /// - Parameters:
127 | /// - left: The left-hand side of the comparison.
128 | /// - right: The right-hand side of the comparison.
129 | /// - Returns: `true` if the left time is less than the right time; otherwise, `false`.
130 | public static func < (left: GregorianTime, right: GregorianTime) -> Bool {
131 | guard left.overflowingDays == right.overflowingDays else { return left.overflowingDays < right.overflowingDays }
132 | guard left.hour == right.hour else { return left.hour < right.hour }
133 | guard left.minute == right.minute else { return left.minute < right.minute }
134 | return left.second < right.second
135 | }
136 | }
137 |
138 | extension GregorianTime {
139 | /// The zero time of day (00:00:00).
140 | public static var zero: Self { GregorianTime(hour: 0, minute: 0, second: 0) }
141 | /// The current time of day.
142 | public static var now: Self { GregorianTime(date: Date()) }
143 | /// Noon (12:00:00).
144 | public static var noon: Self { GregorianTime(hour: 12, minute: 0, second: 0) }
145 | }
146 |
147 | extension GregorianTime: Withable {}
148 |
149 | /// Provides backward compatibility for the renamed `GregorianTime` type.
150 | ///
151 | /// This type has been renamed to ``GregorianTime`` to better reflect its purpose and maintain consistency with other types in the framework.
152 | ///
153 | /// Instead of using `GregorianTimeOfDay`, use ``GregorianTime``:
154 | /// ```swift
155 | /// // Old code:
156 | /// let time = GregorianTimeOfDay(hour: 14, minute: 30)
157 | ///
158 | /// // New code:
159 | /// let time = GregorianTime(hour: 14, minute: 30)
160 | /// ```
161 | @available(*, deprecated, renamed: "GregorianTime", message: "Use GregorianTime instead. This type has been renamed for better clarity and consistency.")
162 | public typealias GregorianTimeOfDay = GregorianTime
163 |
--------------------------------------------------------------------------------
/Sources/HandySwift/Types/HandyRegex.swift:
--------------------------------------------------------------------------------
1 | // Copyright © 2018 Flinesoft. All rights reserved.
2 | // Originally from: https://github.com/sharplet/Regex (modified to remove some weight).
3 |
4 | import Foundation
5 |
6 | /// `HandyRegex` is a swifty regex engine built on top of the NSRegularExpression API.
7 | ///
8 | /// > Warning: The HandyRegex type will be removed in a future version. Migrate to `Swift.Regex` if possible.
9 | ///
10 | /// #### init(_:options:)
11 | ///
12 | /// Initialize with pattern and, optionally, options.
13 | ///
14 | /// ``` swift
15 | /// let regex = try Regex("(Phil|John), [\\d]{4}")
16 | ///
17 | /// let options: Regex.Options = [.ignoreCase, .anchorsMatchLines, .dotMatchesLineSeparators, .ignoreMetacharacters]
18 | /// let regexWithOptions = try Regex("(Phil|John), [\\d]{4}", options: options)
19 | /// ```
20 | ///
21 | /// #### regex.matches(_:)
22 | ///
23 | /// Checks whether regex matches string
24 | ///
25 | /// ``` swift
26 | /// regex.matches("Phil, 1991") // => true
27 | /// ````
28 | ///
29 | /// #### regex.matches(in:)
30 | ///
31 | /// Returns all matches
32 | ///
33 | /// ``` swift
34 | /// regex.matches(in: "Phil, 1991 and John, 1985")
35 | /// // => [Match<"Phil, 1991">, Match<"John, 1985">]
36 | /// ```
37 | ///
38 | /// #### regex.firstMatch(in:)
39 | ///
40 | /// Returns first match if any
41 | ///
42 | /// ``` swift
43 | /// regex.firstMatch(in: "Phil, 1991 and John, 1985")
44 | /// // => Match<"Phil, 1991">
45 | /// ```
46 | ///
47 | /// #### regex.replacingMatches(in:with:count:)
48 | ///
49 | /// Replaces all matches in a string with a template string, up to the a maximum of matches (count).
50 | ///
51 | /// ``` swift
52 | /// regex.replacingMatches(in: "Phil, 1991 and John, 1985", with: "$1 was born in $2", count: 2)
53 | /// // => "Phil was born in 1991 and John was born in 1985"
54 | /// ```
55 | ///
56 | /// #### match.string
57 | ///
58 | /// Returns the captured string
59 | ///
60 | /// ``` swift
61 | /// match.string // => "Phil, 1991"
62 | /// ```
63 | ///
64 | /// #### match.range
65 | ///
66 | /// Returns the range of the captured string within the base string
67 | ///
68 | /// ``` swift
69 | /// match.range // => Range
70 | /// ```
71 | ///
72 | /// #### match.captures
73 | ///
74 | /// Returns the capture groups of a match
75 | ///
76 | /// ``` swift
77 | /// match.captures // => ["Phil", "1991"]
78 | /// ```
79 | ///
80 | /// #### match.string(applyingTemplate:)
81 | ///
82 | /// Replaces the matched string with a template string
83 | ///
84 | /// ``` swift
85 | /// match.string(applyingTemplate: "$1 was born in $2")
86 | /// // => "Phil was born in 1991"
87 | /// ```
88 | public struct HandyRegex {
89 | @usableFromInline
90 | internal let regularExpression: NSRegularExpression
91 |
92 | /// Create a `Regex` based on a pattern string.
93 | ///
94 | /// If `pattern` is not a valid regular expression, an error is thrown
95 | /// describing the failure.
96 | ///
97 | /// - parameters:
98 | /// - pattern: A pattern string describing the regex.
99 | /// - options: Configure regular expression matching options.
100 | /// For details, see `Regex.Options`.
101 | ///
102 | /// - throws: A value of `ErrorType` describing the invalid regular expression.
103 | @available(*, deprecated, message: "The HandyRegex type will be removed in a future version. Migrate to Swift.Regex