Skip to content

Commit

Permalink
Extensions (#335)
Browse files Browse the repository at this point in the history
  • Loading branch information
borut-t authored Dec 24, 2024
1 parent e67fe98 commit 2afc8de
Show file tree
Hide file tree
Showing 18 changed files with 499 additions and 31 deletions.
172 changes: 165 additions & 7 deletions Sources/Core/Extensions/Foundation/Collection+PovioKit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,42 @@ import Foundation

public extension Collection {
/// Returns the element at the specified `index` if it is within bounds, otherwise `nil`.
subscript (safe index: Index) -> Element? {
if startIndex <= index && index < endIndex { self[index] } else { nil }
/// Returns the element at the specified `index` if it is within bounds, otherwise `nil`.
///
/// This subscript safely accesses an element in the collection at the given index. If the index is out of bounds, it returns `nil` instead of causing a runtime error.
///
/// - Parameter index: The index of the element to retrieve.
/// - Returns: The element at the specified index if within bounds, otherwise `nil`.
///
/// ## Example:
/// ```swift
/// let array = [1, 2, 3, 4]
/// let element = array[safe: 2] // returns 3
/// let outOfBounds = array[safe: 10] // returns nil
/// ```
subscript(safe index: Index) -> Element? {
indices.contains(index) ? self[index] : nil
}
}

public extension Collection {
/// Conditional element count - https://forums.swift.org/t/refresh-review-se-0220-count-where/66235/4
/// Returns the count of elements in the collection that satisfy the given predicate.
/// Check for [Reference](https://forums.swift.org/t/refresh-review-se-0220-count-where/66235/4).
///
/// This method applies the provided `predicate` closure to each element in the collection and counts how many elements satisfy the condition.
///
/// - Parameter predicate: A closure that takes an element of the collection and returns a boolean value indicating whether the element satisfies the condition.
/// - Returns: The count of elements that satisfy the predicate.
/// - Throws: This method may throw an error if the `predicate` closure throws an error.
///
/// ## Example
/// ```swift
/// let numbers = [1, 2, 3, 4, 5]
/// let countEven = numbers.count { $0 % 2 == 0 } // counts how many numbers are even
/// Logger.debug(countEven) // output: 2 (since 2 and 4 are even)
/// ```
@inlinable
func count(
where predicate: (Element) throws -> Bool
) rethrows -> Int {
func count(where predicate: (Element) throws -> Bool) rethrows -> Int {
try reduce(0) { n, element in
if try predicate(element) {
n + 1
Expand All @@ -30,7 +55,61 @@ public extension Collection {
}
}

/// Groups collection elements based on dateComponents returning a dictionary
/// Groups the elements of the collection based on extracted date components and returns a dictionary where keys are dates and values are arrays of elements that correspond to those dates.
///
/// This method extracts the date components from each element (using the `extractDate` closure), then groups the elements based on those components.
/// The default date components used for grouping are `.year`, `.month`, and `.day`. The grouping is performed using the provided calendar (or `Calendar.current` by default).
///
/// - Parameters:
/// - extractDate: A closure that extracts a `Date` from each element in the collection.
/// - dateComponents: A set of calendar components used to group the dates. The default is `.year`, `.month`, and `.day`.
/// - calendar: The calendar used to extract and group the date components. The default is `Calendar.current`.
/// - Returns: A dictionary where the keys are `Date` objects representing the grouped date components,
/// and the values are arrays of elements that correspond to each grouped date.
///
/// ## Example
/// ```swift
/// struct Event {
/// var name: String
/// var date: Date
/// }
/// let events = [
/// Event(name: "New Year Party", date: DateFormatter().date(from: "2024-01-01")!),
/// Event(name: "New Year's Morning Run", date: dateFormatter.date(from: "2024-01-01")!),
/// Event(name: "Spring Festival", date: DateFormatter().date(from: "2024-03-20")!),
/// Event(name: "Summer Beach Party", date: DateFormatter().date(from: "2024-06-21")!),
/// Event(name: "Christmas Party", date: dateFormatter.date(from: "2024-12-25")!),
/// Event(name: "Christmas Eve Dinner", date: dateFormatter.date(from: "2024-12-25")!),
/// Event(name: "New Year's Eve", date: DateFormatter().date(from: "2024-12-31")!)
/// ]
/// let groupedEvents = events.grouped(
/// extractDate: { $0.date },
/// by: [.year, .month],
/// calendar: Calendar.current
/// )
/// // Print the grouped events by year and month
/// for (date, eventsInMonth) in groupedEvents {
/// let monthString = DateFormatter.localizedString(from: date, dateStyle: .none, timeStyle: .none)
/// Logger.debug("Events in \(monthString):")
/// for event in eventsInMonth {
/// Logger.debug(" - \(event.name) on \(dateFormatter.string(from: event.date))")
/// }
/// }
///
/// /* Output:
/// Events in 2024-01:
/// - New Year Party on 2024-01-01
/// - New Year's Morning Run on 2024-01-01
/// Events in 2024-03:
/// - Spring Festival on 2024-03-20
/// Events in 2024-06:
/// - Summer Beach Party on 2024-06-21
/// Events in 2024-12:
/// - Christmas Party on 2024-12-25
/// - Christmas Eve Dinner on 2024-12-25
/// - New Year's Eve on 2024-12-31
/// */
/// ```
func grouped(
extractDate: (Element) -> Date,
by dateComponents: Set<Calendar.Component> = [.year, .month, .day],
Expand All @@ -48,6 +127,29 @@ public extension Collection {
}

public extension MutableCollection {
/// A method to mutate each element of a collection using a provided mutator closure.
/// This method allows in-place mutation of the elements in the collection by passing a mutator function that modifies each element.
///
/// - Parameter mutator: A closure that takes a reference to each element in the collection (`inout Element`) and performs the mutation.
/// - Throws: It can throw an error if the `mutator` closure itself throws an error during the mutation process.
///
/// ## Example
/// ```swift
/// var numbers = [1, 2, 3, 4, 5]
///
/// // define a mutator closure that increments each number by 1
/// let increment: (inout Int) throws -> Void = { number in
/// number += 1
/// }
/// do {
///
/// // mutate each number in the array by applying the increment closure
/// try numbers.mutateEach(increment)
/// Logger.debug(numbers) // Output: [2, 3, 4, 5, 6]
/// } catch {
/// Logger.error("Error: \(error)")
/// }
/// ```
mutating func mutateEach(_ mutator: (inout Element) throws -> Void) rethrows {
var idx = startIndex
while idx != endIndex {
Expand All @@ -56,3 +158,59 @@ public extension MutableCollection {
}
}
}

extension MutableCollection where Self: RandomAccessCollection {
/// Updates the element at the specified index with a new value.
///
/// - Parameters:
/// - item: The new value to set at the specified index.
/// - index: The index of the element to update.
/// - Returns: A boolean value indicating whether the update was successful. Returns `true` if the index is valid and the update was performed; otherwise, returns `false`.
///
/// ## Example
/// ```
/// var array = [1, 2, 3, 4]
/// let success = array.update(10, at: 2)
/// // array is now [1, 2, 10, 4]
/// // success is true
///
/// let failure = array.update(10, at: 10)
/// // array remains [1, 2, 10, 4]
/// // failure is false
/// ```
@discardableResult
mutating func update(_ item: Element, at index: Index) -> Bool {
guard indices.contains(index) else { return false }
self[index] = item
return true
}
}

public extension RandomAccessCollection {
/// Splits the collection into chunks of a specified size.
///
/// - Parameter size: The size of each chunk. Each chunk will contain up to `size` elements.
/// - Returns: An array of arrays, where each inner array is a chunk of the original array.
///
/// - Note: If the array's size is not a perfect multiple of `size`, the last chunk will contain the remaining elements.
///
/// ## Example
/// ```
/// let array = [1, 2, 3, 4, 5, 6, 7]
/// let chunks = array.chunked(into: 3)
/// for chunk in collectionChunks {
/// Logger.debug(chunk) // Output: [1, 2, 3], [4, 5, 6], [7]
/// }
/// ```
func chunked(into size: Int) -> [SubSequence] {
guard size > 0 else { return [] }
var result: [SubSequence] = []
var start = startIndex
while start != endIndex {
let end = index(start, offsetBy: size, limitedBy: endIndex) ?? endIndex
result.append(self[start..<end])
start = end
}
return result
}
}
52 changes: 45 additions & 7 deletions Sources/Core/Extensions/Foundation/Data+PovioKit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,63 @@
import Foundation

public extension Data {
/// Returns hexadecimal string representation of data object
/// Usefull for e.g. printing out push notifications `deviceToken`
var hexadecialString: String {
/// Returns a hexadecimal string representation of the data object.
///
/// This property converts each byte of the data object into a two-character hexadecimal string. It is useful for tasks such as displaying raw binary data (e.g., a device token for push notifications) in a human-readable format.
///
/// Useful for printing out device tokens, hashing results, or any scenario where raw data needs to be displayed as a hexadecimal string.
///
/// - Returns: A string containing the hexadecimal representation of the data.
///
/// ## Example
/// ```swift
/// let data = Data([0x01, 0xAB, 0xFF])
/// Logger.debug(data.hexadecimalString) // "01abff"
/// ```
var hexadecimalString: String {
map { String(format: "%02x", $0) }.joined()
}

/// Returns a UTF-8 encoded string.
/// Returns a UTF-8 encoded string from the data object.
///
/// This property attempts to convert the `Data` object into a string using UTF-8 encoding. If the data is not valid UTF-8, it returns `nil`.
///
/// - Returns: A UTF-8 encoded string if the conversion is successful, otherwise `nil`.
///
/// ## Example
/// ```swift
/// let data = Data([0x48, 0x65, 0x6c, 0x6c, 0x6f]) // "Hello" in UTF-8
/// if let string = data.utf8 {
/// Logger.debug(string) // "Hello"
/// }
/// ```
var utf8: String? {
String(data: self, encoding: .utf8)
}

/// Returns a UTF-16 encoded string.
/// Returns a UTF-16 encoded string from the data object.
///
/// This property attempts to convert the `Data` object into a string using UTF-16 encoding. If the data is not valid UTF-16, it returns `nil`.
///
/// - Returns: A UTF-16 encoded string if the conversion is successful, otherwise `nil`.
///
/// - Example:
/// ```swift
/// let data = Data([0x00, 0x48, 0x00, 0x65, 0x00, 0x6c, 0x00, 0x6c, 0x00, 0x6f]) // "Hello" in UTF-16
/// if let string = data.utf16 {
/// Logger.debug(string) // "Hello"
/// }
/// ```
var utf16: String? {
String(data: self, encoding: .utf16)
}

/// Decodes data to given `type` using given `decoder`.
/// ```
/// let decodedUser = responseData.decode(UserResponse.self, with: JSONDecoder())
///
/// # Example
/// ```swift
/// let decodedUser = try responseData.decode(UserResponse.self, with: JSONDecoder())
/// let decodedUser = try JSONDecoder().decode(UserResponse.self, from: responseData)
/// ```
func decode<D: Decodable>(_ type: D.Type, with decoder: JSONDecoder) throws -> D {
try decoder.decode(type, from: self)
Expand Down
34 changes: 32 additions & 2 deletions Sources/Core/Extensions/Foundation/Date+PovioKit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,47 @@
import Foundation

public extension Date {
/// Checks if the date is today.
///
/// - Returns: A boolean value indicating whether the date is today.
var isToday: Bool { calendar.isDateInToday(self) }

/// Checks if the date is yesterday.
///
/// - Returns: A boolean value indicating whether the date is yesterday.
var isYesterday: Bool { calendar.isDateInYesterday(self) }

/// Checks if the date is in the future.
///
/// - Returns: A boolean value indicating whether the date is in the future.
var isInFuture: Bool { self > Date() }

/// Returns first day of the week
/// Gets the year component of the date.
///
/// - Returns: An optional integer representing the year component of the date, or nil if it cannot be determined.
var year: Int? { calendar.dateComponents([.year], from: self).year }

/// Gets the month component of the date.
///
/// - Returns: An optional integer representing the month component of the date, or nil if it cannot be determined.
var month: Int? { calendar.dateComponents([.month], from: self).month }

/// Gets the day component of the date.
///
/// - Returns: An optional integer representing the day component of the date, or nil if it cannot be determined.
var day: Int? { calendar.dateComponents([.day], from: self).day }

/// Gets the start of the week for the date.
///
/// - Returns: An optional Date representing the start of the week, or nil if it cannot be determined.
var startOfWeek: Date? {
let components: Set<Calendar.Component> = [.yearForWeekOfYear, .weekOfYear, .hour, .minute, .second, .nanosecond]
return calendar.date(from: calendar.dateComponents(components, from: self))
}

/// Returns last day of the week
/// Gets the end of the week for the date.
///
/// - Returns: An optional Date representing the end of the week, or nil if it cannot be determined.
var endOfWeek: Date? {
guard let startOfWeek = Date().startOfWeek else { return nil }
return calendar.date(byAdding: .day, value: 6, to: startOfWeek)
Expand Down
Loading

0 comments on commit 2afc8de

Please sign in to comment.