From c8693441f833fd18fe27b4c37ac418e63bb1d0d0 Mon Sep 17 00:00:00 2001 From: Paolo Prodossimo Lopes Date: Mon, 19 May 2025 11:33:43 -0300 Subject: [PATCH 1/4] =?UTF-8?q?=E2=9C=A8=20add=20core=20features?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build_and_test_packages.yml | 11 + .../xcshareddata/xcschemes/EasyCore.xcscheme | 84 ++++ EasyCore.xctestplan | 33 ++ .../EasyCore/Collection/ArrayExtension.swift | 58 +++ .../Collection/ArrayHashableExtension.swift | 32 ++ .../Collection/CollectionExtension.swift | 28 ++ Sources/EasyCore/Debouncer.swift | 58 +++ .../Decoder/DecodableDataExtension.swift | 36 ++ .../Decoder/DecodableStringExtension.swift | 39 ++ .../Decoder/JSONDecoderExtension.swift | 122 +++++ .../EasyCore/Encoder/EncodableExtension.swift | 68 +++ .../Encoder/JSONEncoderExtension.swift | 122 +++++ Sources/EasyCore/MainThread.swift | 49 ++ Sources/EasyCore/Number/DoubleExtension.swift | 65 +++ Sources/EasyCore/Number/FloatExtension.swift | 65 +++ Sources/EasyCore/Number/IntExtension.swift | 65 +++ Sources/EasyCore/Once.swift.swift | 52 ++ .../String/OptionalStringExtension.swift | 30 ++ Sources/EasyCore/String/StringExtension.swift | 101 ++++ Sources/EasyCore/StringExtension.swift | 5 - Sources/EasyCore/Throttler.swift | 57 +++ .../CollectionExtensionTests.swift | 150 ++++++ Tests/EasyCoreTests/Counter.swift | 5 + Tests/EasyCoreTests/DebouncerTests.swift | 69 +++ Tests/EasyCoreTests/DecoderTests.swift | 190 ++++++++ Tests/EasyCoreTests/EasyCoreTests.swift | 6 - Tests/EasyCoreTests/EncoderTests.swift | 105 ++++ .../EasyCoreTests/LocaleExtensionTests.swift | 453 ++++++++++++++++++ .../EasyCoreTests/LocaleIdentifierTests.swift | 451 +++++++++++++++++ Tests/EasyCoreTests/LocaleTests.swift | 12 + Tests/EasyCoreTests/MainThreadTests.swift | 92 ++++ .../EasyCoreTests/NumberExtensionTests.swift | 183 +++++++ Tests/EasyCoreTests/OnceTests.swift | 51 ++ Tests/EasyCoreTests/OptionalTests.swift | 43 ++ Tests/EasyCoreTests/StringTests.swift | 111 +++++ Tests/EasyCoreTests/ThrottlerTests.swift | 73 +++ 36 files changed, 3163 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/build_and_test_packages.yml create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/EasyCore.xcscheme create mode 100644 EasyCore.xctestplan create mode 100644 Sources/EasyCore/Collection/ArrayExtension.swift create mode 100644 Sources/EasyCore/Collection/ArrayHashableExtension.swift create mode 100644 Sources/EasyCore/Collection/CollectionExtension.swift create mode 100644 Sources/EasyCore/Debouncer.swift create mode 100644 Sources/EasyCore/Decoder/DecodableDataExtension.swift create mode 100644 Sources/EasyCore/Decoder/DecodableStringExtension.swift create mode 100644 Sources/EasyCore/Decoder/JSONDecoderExtension.swift create mode 100644 Sources/EasyCore/Encoder/EncodableExtension.swift create mode 100644 Sources/EasyCore/Encoder/JSONEncoderExtension.swift create mode 100644 Sources/EasyCore/MainThread.swift create mode 100644 Sources/EasyCore/Number/DoubleExtension.swift create mode 100644 Sources/EasyCore/Number/FloatExtension.swift create mode 100644 Sources/EasyCore/Number/IntExtension.swift create mode 100644 Sources/EasyCore/Once.swift.swift create mode 100644 Sources/EasyCore/String/OptionalStringExtension.swift create mode 100644 Sources/EasyCore/String/StringExtension.swift delete mode 100644 Sources/EasyCore/StringExtension.swift create mode 100644 Sources/EasyCore/Throttler.swift create mode 100644 Tests/EasyCoreTests/CollectionExtensionTests.swift create mode 100644 Tests/EasyCoreTests/Counter.swift create mode 100644 Tests/EasyCoreTests/DebouncerTests.swift create mode 100644 Tests/EasyCoreTests/DecoderTests.swift delete mode 100644 Tests/EasyCoreTests/EasyCoreTests.swift create mode 100644 Tests/EasyCoreTests/EncoderTests.swift create mode 100644 Tests/EasyCoreTests/LocaleExtensionTests.swift create mode 100644 Tests/EasyCoreTests/LocaleIdentifierTests.swift create mode 100644 Tests/EasyCoreTests/LocaleTests.swift create mode 100644 Tests/EasyCoreTests/MainThreadTests.swift create mode 100644 Tests/EasyCoreTests/NumberExtensionTests.swift create mode 100644 Tests/EasyCoreTests/OnceTests.swift create mode 100644 Tests/EasyCoreTests/OptionalTests.swift create mode 100644 Tests/EasyCoreTests/StringTests.swift create mode 100644 Tests/EasyCoreTests/ThrottlerTests.swift diff --git a/.github/workflows/build_and_test_packages.yml b/.github/workflows/build_and_test_packages.yml new file mode 100644 index 0000000..e2532c2 --- /dev/null +++ b/.github/workflows/build_and_test_packages.yml @@ -0,0 +1,11 @@ +name: Swift Package Manager + +on: + pull_request: + branches: + - main + - develop + +jobs: + use-reusable: + uses: EasyPackages/.github/.github/workflows/build_and_test_packages.yml@main \ No newline at end of file diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/EasyCore.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/EasyCore.xcscheme new file mode 100644 index 0000000..d320184 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/EasyCore.xcscheme @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/EasyCore.xctestplan b/EasyCore.xctestplan new file mode 100644 index 0000000..f198ec2 --- /dev/null +++ b/EasyCore.xctestplan @@ -0,0 +1,33 @@ +{ + "configurations" : [ + { + "id" : "CF935032-BC1F-44BE-B770-A175A1872A51", + "name" : "Test Scheme Action", + "options" : { + + } + } + ], + "defaultOptions" : { + "codeCoverage" : { + "targets" : [ + { + "containerPath" : "container:", + "identifier" : "EasyCore", + "name" : "EasyCore" + } + ] + }, + "testExecutionOrdering" : "random" + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:", + "identifier" : "EasyCoreTests", + "name" : "EasyCoreTests" + } + } + ], + "version" : 1 +} diff --git a/Sources/EasyCore/Collection/ArrayExtension.swift b/Sources/EasyCore/Collection/ArrayExtension.swift new file mode 100644 index 0000000..7c3b1fd --- /dev/null +++ b/Sources/EasyCore/Collection/ArrayExtension.swift @@ -0,0 +1,58 @@ +import Foundation + +public extension Array { + + /// + /// Safely accesses an element at the specified index. + /// + /// Returns the element at the given index, or `nil` if the index is out of bounds. + /// + /// Use this subscript to avoid runtime crashes caused by out-of-range index access, especially when working with dynamic indices. + /// + /// - Parameter index: The index of the desired element. + /// - Returns: The element at the given index, or `nil` if the index is invalid. + /// + /// ### Example: + /// ```swift + /// let names = ["Anna", "Brian", "Carlos"] + /// names[safe: 1] // "Brian" + /// names[safe: 5] // nil + /// ``` + /// + subscript(safe index: Int) -> Element? { + indices.contains(index) ? self[index] : nil + } + + /// + /// Splits the array into multiple subarrays ("chunks") with a maximum specified size. + /// + /// Each subarray will contain at most `size` elements. The last chunk may contain fewer elements if the total count is not a multiple of `size`. + /// + /// - Parameter size: The maximum size of each chunk. Must be greater than zero. + /// - Returns: An array of subarrays containing the original elements in order. + /// + /// ### Example: + /// ```swift + /// let numbers = [1, 2, 3, 4, 5, 6, 7] + /// let chunks = numbers.chunked(by: 3) + /// // Result: [[1, 2, 3], [4, 5, 6], [7]] + /// ``` + /// + /// - Note: If `size <= 0`, the result will be an empty array (`[]`). + /// + func chunked(by size: Int) -> [[Element]] { + guard size > 0 else { return [] } + + var chunks: [[Element]] = [] + var currentIndex = 0 + + while currentIndex < count { + let endIndex = Swift.min(currentIndex + size, count) + let chunk = Array(self[currentIndex.. = [] + return filter { seen.insert($0).inserted } + } +} diff --git a/Sources/EasyCore/Collection/CollectionExtension.swift b/Sources/EasyCore/Collection/CollectionExtension.swift new file mode 100644 index 0000000..0c267ed --- /dev/null +++ b/Sources/EasyCore/Collection/CollectionExtension.swift @@ -0,0 +1,28 @@ +import Foundation + +public extension Collection { + + /// + /// Checks whether a given optional collection is either `nil` or empty. + /// + /// This static method is a convenient utility for validating optional collections in a safe and concise way. + /// + /// - Parameter collection: An optional collection of the same type as `Self`. + /// - Returns: `true` if the collection is `nil` or contains no elements; otherwise, `false`. + /// + /// ### Example: + /// ```swift + /// let items: [String]? = nil + /// Collection.isNilOrEmpty(items) // true + /// + /// let emptyList: [Int]? = [] + /// Collection.isNilOrEmpty(emptyList) // true + /// + /// let numbers: [Int]? = [1, 2, 3] + /// Collection.isNilOrEmpty(numbers) // false + /// ``` + /// + static func isNilOrEmpty(_ collection: Self?) -> Bool { + collection?.isEmpty ?? true + } +} diff --git a/Sources/EasyCore/Debouncer.swift b/Sources/EasyCore/Debouncer.swift new file mode 100644 index 0000000..b9758db --- /dev/null +++ b/Sources/EasyCore/Debouncer.swift @@ -0,0 +1,58 @@ +import Foundation + +/// +/// A utility class that delays the execution of a block of code until a specified time interval has passed since the last call. +/// +/// Useful for scenarios where you want to limit the number of times a function is executed, +/// such as reacting to user input, search queries, or other high-frequency events. +/// +/// Each call to `call(_:)` resets the timer. The block will only be executed if no further calls are made within the delay interval. +/// +public final class Debouncer { + + /// + /// The delay interval before the block is executed. + /// + private let delay: TimeInterval + + /// + /// The currently scheduled work item, if any. + /// + private var workItem: DispatchWorkItem? + + /// + /// Initializes a new `Debouncer` with the specified delay interval. + /// + /// - Parameter delay: The time interval (in seconds) to wait after the last call before executing the block. + /// + /// ### Example: + /// ```swift + /// let debouncer = Debouncer(delay: 0.3) + /// ``` + /// + public init(delay: TimeInterval) { + self.delay = delay + } + + /// + /// Schedules a block to be executed after the specified delay. + /// + /// If this method is called again before the delay elapses, the previous block is cancelled and the timer resets. + /// + /// - Parameter block: The closure to execute after the delay. + /// + /// ### Example: + /// ```swift + /// debouncer.call { + /// print("This will only print if no other call occurs in the next 0.3 seconds.") + /// } + /// ``` + /// + public func call(_ block: @escaping () -> Void) { + workItem?.cancel() + workItem = DispatchWorkItem(block: block) + if let workItem = workItem { + DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: workItem) + } + } +} diff --git a/Sources/EasyCore/Decoder/DecodableDataExtension.swift b/Sources/EasyCore/Decoder/DecodableDataExtension.swift new file mode 100644 index 0000000..5148722 --- /dev/null +++ b/Sources/EasyCore/Decoder/DecodableDataExtension.swift @@ -0,0 +1,36 @@ +import Foundation + +public extension Decodable where Self == Data { + + /// + /// Decodes the current `Data` instance into a specified `Decodable` type. + /// + /// This method provides a convenient way to decode JSON data directly into a model object using a provided or default `JSONDecoder`. + /// + /// - Parameters: + /// - type: The `Decodable` type to decode from the data. + /// - decoder: An optional `JSONDecoder` to use. Defaults to `.standard`. + /// - Returns: An instance of the decoded type `T` if decoding is successful; otherwise, `nil`. + /// + /// ### Example: + /// ```swift + /// struct User: Decodable { + /// let name: String + /// let age: Int + /// } + /// + /// let jsonData = """ + /// { + /// "name": "Letícia", + /// "age": 28 + /// } + /// """.data(using: .utf8)! + /// + /// let user = jsonData.decode(User.self) + /// // user is of type `User?` + /// ``` + /// + func decode(_ type: T.Type, decoder: JSONDecoder = .standard) -> T? { + try? decoder.decode(T.self, from: self) + } +} diff --git a/Sources/EasyCore/Decoder/DecodableStringExtension.swift b/Sources/EasyCore/Decoder/DecodableStringExtension.swift new file mode 100644 index 0000000..39f2fbb --- /dev/null +++ b/Sources/EasyCore/Decoder/DecodableStringExtension.swift @@ -0,0 +1,39 @@ +import Foundation + +public extension Decodable where Self == String { + + /// + /// Decodes the current `String` instance, assumed to be a JSON string, into a specified `Decodable` type. + /// + /// This method first attempts to convert the string to UTF-8 encoded `Data`, then decodes it into the desired type using the provided or default `JSONDecoder`. + /// + /// - Parameters: + /// - type: The `Decodable` type you want to decode from the JSON string. + /// - decoder: An optional `JSONDecoder` to use for decoding. Defaults to `.standard`. + /// - Returns: An instance of the decoded type `T` if decoding is successful; otherwise, `nil`. + /// + /// ### Example: + /// ```swift + /// struct User: Decodable { + /// let name: String + /// let age: Int + /// } + /// + /// let json = """ + /// { + /// "name": "Letícia", + /// "age": 28 + /// } + /// """ + /// + /// let user = json.decode(User.self) + /// // user is of type `User?` + /// ``` + /// + /// - Note: This method fails silently and returns `nil` if the string cannot be converted to UTF-8 `Data` or if decoding fails. + /// + func decode(_ type: T.Type, decoder: JSONDecoder = .standard) -> T? { + guard let data = self.data(using: .utf8) else { return nil } + return data.decode(T.self, decoder: decoder) + } +} diff --git a/Sources/EasyCore/Decoder/JSONDecoderExtension.swift b/Sources/EasyCore/Decoder/JSONDecoderExtension.swift new file mode 100644 index 0000000..a60f8ee --- /dev/null +++ b/Sources/EasyCore/Decoder/JSONDecoderExtension.swift @@ -0,0 +1,122 @@ +import Foundation + +/// +/// A set of convenient static and instance properties to create preconfigured `JSONDecoder` instances +/// with commonly used strategies for decoding keys and dates. +/// +public extension JSONDecoder { + + /// + /// A standard `JSONDecoder` instance with default decoding strategies. + /// + /// This is the base decoder without any custom strategies applied. + /// + /// ### Example: + /// ```swift + /// let decoder = JSONDecoder.standard + /// ``` + /// + @inlinable static var standard: JSONDecoder { + JSONDecoder() + } + + /// + /// A `JSONDecoder` configured to decode snake_case keys into camelCase properties. + /// + /// Equivalent to setting `.keyDecodingStrategy = .convertFromSnakeCase`. + /// + /// ### Example: + /// ```swift + /// let decoder = JSONDecoder.snakeCase + /// ``` + /// + static var snakeCase: JSONDecoder { + standard.snakeCase + } + + /// + /// A `JSONDecoder` configured to decode ISO 8601 date strings. + /// + /// Equivalent to setting `.dateDecodingStrategy = .iso8601`. + /// + /// ### Example: + /// ```swift + /// let decoder = JSONDecoder.iso8601 + /// ``` + /// + static var iso8601: JSONDecoder { + standard.iso8601 + } + + /// + /// A `JSONDecoder` configured to decode dates as seconds since 1970 (Unix timestamp). + /// + /// Equivalent to setting `.dateDecodingStrategy = .secondsSince1970`. + /// + /// ### Example: + /// ```swift + /// let decoder = JSONDecoder.secondsSince1970 + /// ``` + /// + static var secondsSince1970: JSONDecoder { + standard.secondsSince1970 + } + + /// + /// Creates a `JSONDecoder` configured to decode dates using a custom `DateFormatter`. + /// + /// - Parameter formatter: The `DateFormatter` used to parse date strings. + /// - Returns: A configured `JSONDecoder` instance. + /// + /// ### Example: + /// ```swift + /// let formatter = DateFormatter() + /// formatter.dateFormat = "yyyy-MM-dd" + /// let decoder = JSONDecoder.formatted(using: formatter) + /// ``` + /// + static func formatted(using formatter: DateFormatter) -> JSONDecoder { + standard.formatted(using: formatter) + } + + /// + /// Applies snake_case key decoding to the current decoder instance. + /// + /// - Returns: The same decoder with `.convertFromSnakeCase` strategy applied. + /// + var snakeCase: JSONDecoder { + keyDecodingStrategy = .convertFromSnakeCase + return self + } + + /// + /// Applies ISO 8601 date decoding to the current decoder instance. + /// + /// - Returns: The same decoder with `.iso8601` date strategy applied. + /// + var iso8601: JSONDecoder { + dateDecodingStrategy = .iso8601 + return self + } + + /// + /// Applies Unix timestamp (seconds since 1970) date decoding to the current decoder instance. + /// + /// - Returns: The same decoder with `.secondsSince1970` date strategy applied. + /// + var secondsSince1970: JSONDecoder { + dateDecodingStrategy = .secondsSince1970 + return self + } + + /// + /// Applies a custom `DateFormatter` for decoding dates to the current decoder instance. + /// + /// - Parameter formatter: The `DateFormatter` to use. + /// - Returns: The same decoder with `.formatted(formatter)` date decoding strategy applied. + /// + func formatted(using formatter: DateFormatter) -> JSONDecoder { + dateDecodingStrategy = .formatted(formatter) + return self + } +} diff --git a/Sources/EasyCore/Encoder/EncodableExtension.swift b/Sources/EasyCore/Encoder/EncodableExtension.swift new file mode 100644 index 0000000..803c518 --- /dev/null +++ b/Sources/EasyCore/Encoder/EncodableExtension.swift @@ -0,0 +1,68 @@ +import Foundation + +/// +/// Utility extensions for `Encodable` types to simplify JSON encoding and conversions to common formats. +/// +public extension Encodable { + + /// + /// Encodes the current instance into `Data` using the specified `JSONEncoder`. + /// + /// - Parameter encoder: The `JSONEncoder` to use. Defaults to `.standard`. + /// - Returns: A `Data` representation of the encoded object, or `nil` if encoding fails. + /// + /// ### Example: + /// ```swift + /// struct User: Encodable { + /// let name: String + /// let age: Int + /// } + /// + /// let user = User(name: "Letícia", age: 28) + /// let data = user.encode() + /// ``` + /// + func encode(encoder: JSONEncoder = .standard) -> Data? { + try? encoder.encode(self) + } + + /// + /// Encodes the current instance into a JSON-formatted `String`. + /// + /// - Parameter prettyPrinted: If `true`, the output will be formatted with line breaks and indentation. Defaults to `false`. + /// - Returns: A JSON `String` representation of the object, or `nil` if encoding fails. + /// + /// ### Example: + /// ```swift + /// let user = User(name: "Letícia", age: 28) + /// let json = user.encodeToString(prettyPrinted: true) + /// ``` + /// + func encodeToString(prettyPrinted: Bool = false) -> String? { + let encoder = JSONEncoder() + if prettyPrinted { + encoder.outputFormatting = [.prettyPrinted, .sortedKeys] + } + guard let data = try? encoder.encode(self) else { return nil } + return String(data: data, encoding: .utf8) + } + + /// + /// Converts the current instance into a `[String: Any]` dictionary using `JSONSerialization`. + /// + /// This property encodes the instance to `Data`, then deserializes it back into a dictionary. + /// + /// - Returns: A dictionary representation of the object, or `nil` if encoding or serialization fails. + /// + /// ### Example: + /// ```swift + /// let user = User(name: "Letícia", age: 28) + /// let dict = user.dictionary + /// // dict = ["name": "Letícia", "age": 28] + /// ``` + /// + var dictionary: [String: Any]? { + guard let data = encode() else { return nil } + return (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] + } +} diff --git a/Sources/EasyCore/Encoder/JSONEncoderExtension.swift b/Sources/EasyCore/Encoder/JSONEncoderExtension.swift new file mode 100644 index 0000000..73b0cf2 --- /dev/null +++ b/Sources/EasyCore/Encoder/JSONEncoderExtension.swift @@ -0,0 +1,122 @@ +import Foundation + +/// +/// A set of convenient static and instance properties to create preconfigured `JSONEncoder` +/// instances with commonly used strategies for encoding keys and dates. +/// +public extension JSONEncoder { + + /// + /// A standard `JSONEncoder` instance with default encoding strategies. + /// + /// This encoder uses the default key and date encoding strategies. + /// + /// ### Example: + /// ```swift + /// let encoder = JSONEncoder.standard + /// ``` + /// + @inlinable static var standard: JSONEncoder { + JSONEncoder() + } + + /// + /// A `JSONEncoder` configured to convert camelCase keys to snake_case when encoding. + /// + /// Equivalent to setting `.keyEncodingStrategy = .convertToSnakeCase`. + /// + /// ### Example: + /// ```swift + /// let encoder = JSONEncoder.snakeCase + /// ``` + /// + static var snakeCase: JSONEncoder { + standard.snakeCase + } + + /// + /// A `JSONEncoder` configured to encode dates using ISO 8601 format. + /// + /// Equivalent to setting `.dateEncodingStrategy = .iso8601`. + /// + /// ### Example: + /// ```swift + /// let encoder = JSONEncoder.iso8601 + /// ``` + /// + static var iso8601: JSONEncoder { + standard.iso8601 + } + + /// + /// A `JSONEncoder` configured to encode dates as seconds since 1970 (Unix timestamp). + /// + /// Equivalent to setting `.dateEncodingStrategy = .secondsSince1970`. + /// + /// ### Example: + /// ```swift + /// let encoder = JSONEncoder.secondsSince1970 + /// ``` + /// + static var secondsSince1970: JSONEncoder { + standard.secondsSince1970 + } + + /// + /// Creates a `JSONEncoder` configured to encode dates using a custom `DateFormatter`. + /// + /// - Parameter formatter: The `DateFormatter` to use when encoding `Date` values. + /// - Returns: A configured `JSONEncoder` instance. + /// + /// ### Example: + /// ```swift + /// let formatter = DateFormatter() + /// formatter.dateFormat = "yyyy-MM-dd" + /// let encoder = JSONEncoder.formatted(using: formatter) + /// ``` + /// + static func formatted(using formatter: DateFormatter) -> JSONEncoder { + standard.formatted(using: formatter) + } + + /// + /// Applies the `.convertToSnakeCase` key encoding strategy to the current encoder instance. + /// + /// - Returns: The same encoder instance with `.convertToSnakeCase` applied. + /// + var snakeCase: JSONEncoder { + keyEncodingStrategy = .convertToSnakeCase + return self + } + + /// + /// Applies the `.iso8601` date encoding strategy to the current encoder instance. + /// + /// - Returns: The same encoder instance with `.iso8601` applied. + /// + var iso8601: JSONEncoder { + dateEncodingStrategy = .iso8601 + return self + } + + /// + /// Applies the `.secondsSince1970` date encoding strategy to the current encoder instance. + /// + /// - Returns: The same encoder instance with `.secondsSince1970` applied. + /// + var secondsSince1970: JSONEncoder { + dateEncodingStrategy = .secondsSince1970 + return self + } + + /// + /// Applies a custom `DateFormatter` to encode date values in the current encoder instance. + /// + /// - Parameter formatter: The `DateFormatter` to use. + /// - Returns: The same encoder instance with the custom formatter applied. + /// + func formatted(using formatter: DateFormatter) -> JSONEncoder { + dateEncodingStrategy = .formatted(formatter) + return self + } +} diff --git a/Sources/EasyCore/MainThread.swift b/Sources/EasyCore/MainThread.swift new file mode 100644 index 0000000..9879c1b --- /dev/null +++ b/Sources/EasyCore/MainThread.swift @@ -0,0 +1,49 @@ +import Foundation + +/// +/// A utility namespace that provides safe and convenient methods for executing code on the main thread. +/// +public enum MainThread { + + /// + /// Executes a block of code on the main thread, immediately if already on it, or asynchronously if not. + /// + /// This ensures thread-safety when updating UI or performing main-thread-bound operations from a background thread. + /// + /// - Parameter block: A `@Sendable` closure to execute on the main thread. + /// + /// ### Example: + /// ```swift + /// MainThread.asyncSafe { + /// self.label.text = "Updated safely" + /// } + /// ``` + /// + public static func asyncSafe(_ block: @Sendable @escaping () -> Void) { + if Thread.isMainThread { + block() + } else { + DispatchQueue.main.async { [block] in + block() + } + } + } + + /// + /// Executes a block of code on the main thread after a specified delay. + /// + /// - Parameters: + /// - seconds: The delay in seconds before the block is executed. + /// - block: A `@Sendable` closure to execute after the delay. + /// + /// ### Example: + /// ```swift + /// MainThread.asyncAfter(seconds: 1.0) { + /// print("Executed after 1 second on the main thread") + /// } + /// ``` + /// + public static func asyncAfter(seconds: TimeInterval, _ block: @Sendable @escaping () -> Void) { + DispatchQueue.main.asyncAfter(deadline: .now() + seconds, execute: block) + } +} diff --git a/Sources/EasyCore/Number/DoubleExtension.swift b/Sources/EasyCore/Number/DoubleExtension.swift new file mode 100644 index 0000000..c09c130 --- /dev/null +++ b/Sources/EasyCore/Number/DoubleExtension.swift @@ -0,0 +1,65 @@ +import Foundation + +public extension Double { + + /// + /// Indicates whether the value is greater than zero. + /// + /// - Returns: `true` if the value is positive; otherwise, `false`. + /// + /// ### Example: + /// ```swift + /// let value = 3.14 + /// value.isPositive // true + /// ``` + /// + var isPositive: Bool { + self > 0 + } + + /// + /// Indicates whether the value is less than zero. + /// + /// - Returns: `true` if the value is negative; otherwise, `false`. + /// + /// ### Example: + /// ```swift + /// let value = -7.5 + /// value.isNegative // true + /// ``` + /// + var isNegative: Bool { + self < 0 + } + + /// + /// Indicates whether the value is exactly zero. + /// + /// - Returns: `true` if the value is zero; otherwise, `false`. + /// + /// ### Example: + /// ```swift + /// let value = 0.0 + /// value.isZero // true + /// ``` + /// + var isZero: Bool { + self == 0 + } + + /// + /// Checks whether the current value falls within the specified closed range. + /// + /// - Parameter range: A closed range (`ClosedRange`) to check against. + /// - Returns: `true` if the value is within the range (inclusive); otherwise, `false`. + /// + /// ### Example: + /// ```swift + /// let temperature = 22.5 + /// temperature.isInRange(20.0...25.0) // true + /// ``` + /// + func isInRange(_ range: ClosedRange) -> Bool { + range.contains(self) + } +} diff --git a/Sources/EasyCore/Number/FloatExtension.swift b/Sources/EasyCore/Number/FloatExtension.swift new file mode 100644 index 0000000..3a18959 --- /dev/null +++ b/Sources/EasyCore/Number/FloatExtension.swift @@ -0,0 +1,65 @@ +import Foundation + +public extension Float { + + /// + /// Indicates whether the value is greater than zero. + /// + /// - Returns: `true` if the value is positive; otherwise, `false`. + /// + /// ### Example: + /// ```swift + /// let value: Float = 3.14 + /// value.isPositive // true + /// ``` + /// + var isPositive: Bool { + self > 0 + } + + /// + /// Indicates whether the value is less than zero. + /// + /// - Returns: `true` if the value is negative; otherwise, `false`. + /// + /// ### Example: + /// ```swift + /// let value: Float = -2.7 + /// value.isNegative // true + /// ``` + /// + var isNegative: Bool { + self < 0 + } + + /// + /// Indicates whether the value is exactly zero. + /// + /// - Returns: `true` if the value is zero; otherwise, `false`. + /// + /// ### Example: + /// ```swift + /// let value: Float = 0.0 + /// value.isZero // true + /// ``` + /// + var isZero: Bool { + self == 0 + } + + /// + /// Checks whether the current value falls within the specified closed range. + /// + /// - Parameter range: A closed range (`ClosedRange`) to check against. + /// - Returns: `true` if the value is within the range (inclusive); otherwise, `false`. + /// + /// ### Example: + /// ```swift + /// let score: Float = 7.5 + /// score.isInRange(0.0...10.0) // true + /// ``` + /// + func isInRange(_ range: ClosedRange) -> Bool { + range.contains(self) + } +} diff --git a/Sources/EasyCore/Number/IntExtension.swift b/Sources/EasyCore/Number/IntExtension.swift new file mode 100644 index 0000000..0dda853 --- /dev/null +++ b/Sources/EasyCore/Number/IntExtension.swift @@ -0,0 +1,65 @@ +import Foundation + +public extension Int { + + /// + /// Indicates whether the value is greater than zero. + /// + /// - Returns: `true` if the value is positive; otherwise, `false`. + /// + /// ### Example: + /// ```swift + /// let count = 5 + /// count.isPositive // true + /// ``` + /// + var isPositive: Bool { + self > 0 + } + + /// + /// Indicates whether the value is less than zero. + /// + /// - Returns: `true` if the value is negative; otherwise, `false`. + /// + /// ### Example: + /// ```swift + /// let delta = -3 + /// delta.isNegative // true + /// ``` + /// + var isNegative: Bool { + self < 0 + } + + /// + /// Indicates whether the value is exactly zero. + /// + /// - Returns: `true` if the value is zero; otherwise, `false`. + /// + /// ### Example: + /// ```swift + /// let total = 0 + /// total.isZero // true + /// ``` + /// + var isZero: Bool { + self == 0 + } + + /// + /// Checks whether the current value falls within the specified closed range. + /// + /// - Parameter range: A closed range (`ClosedRange`) to check against. + /// - Returns: `true` if the value is within the range (inclusive); otherwise, `false`. + /// + /// ### Example: + /// ```swift + /// let score = 85 + /// score.isInRange(0...100) // true + /// ``` + /// + func isInRange(_ range: ClosedRange) -> Bool { + range.contains(self) + } +} diff --git a/Sources/EasyCore/Once.swift.swift b/Sources/EasyCore/Once.swift.swift new file mode 100644 index 0000000..1d71ba7 --- /dev/null +++ b/Sources/EasyCore/Once.swift.swift @@ -0,0 +1,52 @@ +import Foundation + +/// +/// A utility that ensures a block of code is executed only once, in a thread-safe manner. +/// +/// Use this class when you need to perform an action a single time during the lifecycle of your app, +/// such as initializing a singleton resource, logging once, or performing a setup task. +/// +public final class Once: @unchecked Sendable { + private var hasRun = false + private let lock = NSLock() + + /// + /// Creates a new `Once` instance. + /// + /// ### Example: + /// ```swift + /// let once = Once() + /// ``` + /// + public init() {} + + /// + /// Executes the given block only once. Subsequent calls to this method will do nothing. + /// + /// This method is thread-safe and ensures the block is executed only a single time, + /// even when accessed from multiple threads concurrently. + /// + /// - Parameter block: The closure to execute once. + /// + /// ### Example: + /// ```swift + /// let once = Once() + /// + /// once.run { + /// print("Executed only once") + /// } + /// + /// once.run { + /// print("Will not be executed again") + /// } + /// ``` + /// + public func run(_ block: () -> Void) { + lock.lock() + defer { lock.unlock() } + + guard !hasRun else { return } + hasRun = true + block() + } +} diff --git a/Sources/EasyCore/String/OptionalStringExtension.swift b/Sources/EasyCore/String/OptionalStringExtension.swift new file mode 100644 index 0000000..471150d --- /dev/null +++ b/Sources/EasyCore/String/OptionalStringExtension.swift @@ -0,0 +1,30 @@ +import Foundation + +public extension Optional where Wrapped == String { + + /// + /// Indicates whether the optional string is `nil`, empty, or contains only whitespace and newline characters. + /// + /// This is useful for validating optional text input where both `nil` and `" "` should be treated as blank. + /// + /// - Returns: `true` if the string is `nil` or blank; otherwise, `false`. + /// + /// ### Example: + /// ```swift + /// let name: String? = nil + /// name.isBlank // true + /// + /// let email: String? = " " + /// email.isBlank // true + /// + /// let username: String? = "John" + /// username.isBlank // false + /// ``` + /// + var isBlank: Bool { + switch self { + case .none: true + case .some(let value): value.isBlank + } + } +} diff --git a/Sources/EasyCore/String/StringExtension.swift b/Sources/EasyCore/String/StringExtension.swift new file mode 100644 index 0000000..b300b25 --- /dev/null +++ b/Sources/EasyCore/String/StringExtension.swift @@ -0,0 +1,101 @@ +import Foundation + +public extension String { + + /// + /// Removes all occurrences of the specified substring from the current string. + /// + /// - Parameter value: The substring to be removed. + /// - Returns: A new string with all instances of `value` removed. + /// + /// ### Example: + /// ```swift + /// let result = "123.45 BRL".removeOcurrencing("BRL") + /// // result == "123.45 " + /// ``` + /// + func removeOcurrencing(_ value: String) -> String { + replacingOccurrences(of: value, with: String()) + } + + /// + /// Indicates whether the string contains only numeric characters. + /// + /// - Returns: `true` if the string is not blank and consists entirely of digits; otherwise, `false`. + /// + /// ### Example: + /// ```swift + /// "12345".isNumeric // true + /// "12a45".isNumeric // false + /// "".isNumeric // false + /// ``` + /// + var isNumeric: Bool { + return !isBlank && allSatisfy { $0.isNumber } + } + + /// + /// Indicates whether the string contains only alphabetic characters. + /// + /// - Returns: `true` if the string is not blank and consists entirely of letters; otherwise, `false`. + /// + /// ### Example: + /// ```swift + /// "Hello".isAlphabetic // true + /// "Hello1".isAlphabetic // false + /// " ".isAlphabetic // false + /// ``` + /// + var isAlphabetic: Bool { + return !isBlank && allSatisfy { $0.isLetter } + } + + /// + /// Indicates whether the string is empty or consists only of whitespace and newline characters. + /// + /// - Returns: `true` if the string is blank; otherwise, `false`. + /// + /// ### Example: + /// ```swift + /// "".isBlank // true + /// " \n".isBlank // true + /// "Hello".isBlank // false + /// ``` + /// + var isBlank: Bool { + trimmed().isEmpty + } + + /// + /// Returns a copy of the string with leading and trailing whitespace and newline characters removed. + /// + /// - Returns: A trimmed version of the string. + /// + /// ### Example: + /// ```swift + /// " Hello \n".trimmed() // "Hello" + /// ``` + /// + func trimmed() -> String { + trimmingCharacters(in: .whitespacesAndNewlines) + } + + /// + /// Indicates whether the string is in a valid email format. + /// + /// - Returns: `true` if the string matches a basic email pattern; otherwise, `false`. + /// + /// ### Example: + /// ```swift + /// "user@example.com".isEmail // true + /// "invalid-email.com".isEmail // false + /// ``` + /// + var isEmail: Bool { + NSPredicate( + format: "SELF MATCHES %@", + #"^[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$"# + ) + .evaluate(with: self) + } +} diff --git a/Sources/EasyCore/StringExtension.swift b/Sources/EasyCore/StringExtension.swift deleted file mode 100644 index c76f585..0000000 --- a/Sources/EasyCore/StringExtension.swift +++ /dev/null @@ -1,5 +0,0 @@ -public extension String { - func removeOcurrencing(_ value: String) -> String { - replacingOccurrences(of: value, with: String()) - } -} diff --git a/Sources/EasyCore/Throttler.swift b/Sources/EasyCore/Throttler.swift new file mode 100644 index 0000000..7bedc13 --- /dev/null +++ b/Sources/EasyCore/Throttler.swift @@ -0,0 +1,57 @@ +import Foundation + +/// +/// A utility that limits the rate at which a block of code can be executed. +/// +/// Use `Throttler` to prevent a block from being called more than once within a specified time interval. +/// This is useful for scenarios like handling rapid user input, scroll events, or network requests where excessive frequency can be problematic. +/// +public final class Throttler { + private let delay: TimeInterval + private var lastExecution: Date? + private var workItem: DispatchWorkItem? + + /// + /// Creates a new `Throttler` instance with the given delay. + /// + /// - Parameter delay: The minimum time interval (in seconds) between allowed executions. + /// + /// ### Example: + /// ```swift + /// let throttler = Throttler(delay: 1.0) + /// ``` + /// + public init(delay: TimeInterval) { + self.delay = delay + } + + /// + /// Attempts to execute the given block if enough time has passed since the last execution. + /// + /// If the time since the last execution is less than the configured delay, the block is ignored. + /// + /// - Parameter block: The closure to execute, if allowed. + /// + /// ### Example: + /// ```swift + /// throttler.call { + /// print("Executed at most once per second") + /// } + /// ``` + /// + public func call(_ block: @escaping () -> Void) { + let now = Date() + + if let last = lastExecution, now.timeIntervalSince(last) < delay { + return + } + + workItem?.cancel() + workItem = DispatchWorkItem(block: block) + lastExecution = now + + if let workItem = workItem { + DispatchQueue.main.async(execute: workItem) + } + } +} diff --git a/Tests/EasyCoreTests/CollectionExtensionTests.swift b/Tests/EasyCoreTests/CollectionExtensionTests.swift new file mode 100644 index 0000000..923e73c --- /dev/null +++ b/Tests/EasyCoreTests/CollectionExtensionTests.swift @@ -0,0 +1,150 @@ +import Testing + +@testable import EasyCore + +@Suite("Collection") +struct CollectionExtensionTests { + @Suite("Array") + struct ArrayTests { + @Test("[safe:]") + func sage() { + let a = [1, 2, 3] + #expect(a[safe: 0] == 1) + #expect(a[safe: 1] == 2) + #expect(a[safe: 2] == 3) + #expect(a[safe: 3] == nil) + } + + @Suite(".chunked(by:)") + struct ChunkedTests { + + @Test("Returns chunks of equal size when array size is divisible by chunk size") + func equalChunks() { + let input = [1, 2, 3, 4, 5, 6] + let result = input.chunked(by: 2) + #expect(result == [[1, 2], [3, 4], [5, 6]]) + } + + @Test("Returns last chunk with fewer elements when not divisible") + func unevenChunks() { + let input = [1, 2, 3, 4, 5] + let result = input.chunked(by: 2) + #expect(result == [[1, 2], [3, 4], [5]]) + } + + @Test("Returns single chunk when chunk size is greater than array count") + func chunkSizeGreaterThanArray() { + let input = [1, 2, 3] + let result = input.chunked(by: 10) + #expect(result == [[1, 2, 3]]) + } + + @Test("Returns empty array when chunk size is zero") + func zeroChunkSize() { + let input = [1, 2, 3] + let result = input.chunked(by: 0) + #expect(result.isEmpty) + } + + @Test("Returns empty array when chunk size is negative") + func negativeChunkSize() { + let input = [1, 2, 3] + let result = input.chunked(by: -1) + #expect(result.isEmpty) + } + + @Test("Returns empty array when input is empty") + func emptyArray() { + let input: [Int] = [] + let result = input.chunked(by: 3) + #expect(result == []) + } + + @Test("Returns correct number of chunks") + func numberOfChunks() { + let input = Array(1...10) + let result = input.chunked(by: 4) + #expect(result.count == 3) + } + } + + @Suite(".uniqued") + struct UniquedTests { + + @Test("Removes duplicated integers while preserving order") + func integerArray() { + let input = [1, 2, 2, 3, 1, 4] + let result = input.uniqued + #expect(result == [1, 2, 3, 4]) + } + + @Test("Removes duplicated strings while preserving order") + func stringArray() { + let input = ["apple", "banana", "apple", "orange"] + let result = input.uniqued + #expect(result == ["apple", "banana", "orange"]) + } + + @Test("Returns same array when all elements are unique") + func allUnique() { + let input = [1, 2, 3, 4, 5] + let result = input.uniqued + #expect(result == input) + } + + @Test("Returns empty array when input is empty") + func emptyArray() { + let input: [Int] = [] + let result = input.uniqued + #expect(result.isEmpty) + } + + @Test("Removes repeated characters in order") + func characterArray() { + let input: [Character] = ["a", "b", "a", "c", "b"] + let result = input.uniqued + #expect(result == ["a", "b", "c"]) + } + } + } + + @Suite(".isNilOrEmpty") + struct IsNilOrEmptyTests { + + @Test("Returns true for nil array") + func nilArray() { + let array: [Int]? = nil + #expect(Array.isNilOrEmpty(array)) + } + + @Test("Returns true for empty array") + func emptyArray() { + let array: [String]? = [] + #expect(Array.isNilOrEmpty(array)) + } + + @Test("Returns false for non-empty array") + func nonEmptyArray() { + let array: [Int]? = [1, 2, 3] + #expect(!Array.isNilOrEmpty(array)) + } + + @Test("Returns true for nil set") + func nilSet() { + let set: Set? = nil + #expect(Set.isNilOrEmpty(set)) + } + + @Test("Returns true for empty set") + func emptySet() { + let set: Set? = [] + #expect(Set.isNilOrEmpty(set)) + } + + @Test("Returns false for non-empty set") + func nonEmptySet() { + let set: Set? = ["A", "B"] + #expect(!Set.isNilOrEmpty(set)) + } + } +} diff --git a/Tests/EasyCoreTests/Counter.swift b/Tests/EasyCoreTests/Counter.swift new file mode 100644 index 0000000..d90f14a --- /dev/null +++ b/Tests/EasyCoreTests/Counter.swift @@ -0,0 +1,5 @@ +actor Counter { + private var count = 0 + func increment() { count += 1 } + func value() -> Int { count } +} diff --git a/Tests/EasyCoreTests/DebouncerTests.swift b/Tests/EasyCoreTests/DebouncerTests.swift new file mode 100644 index 0000000..5f6417b --- /dev/null +++ b/Tests/EasyCoreTests/DebouncerTests.swift @@ -0,0 +1,69 @@ +import Foundation +import Testing + +@testable import EasyCore + +@Suite("Debouncer") +struct DebouncerTests { + + @Test("Executes block after delay") + func executesAfterDelay() async throws { + let debouncer = Debouncer(delay: 0.2) + var didRun = false + + debouncer.call { + didRun = true + } + + try await Task.sleep(nanoseconds: 300_000_000) // 0.3s + #expect(didRun) + } + + @Test("Cancels previous block if called again before delay") + func cancelsPreviousBlock() async throws { + let debouncer = Debouncer(delay: 0.3) + var counter = 0 + + debouncer.call { + counter += 1 + } + + try await Task.sleep(nanoseconds: 100_000_000) // 0.1s + + debouncer.call { + counter += 1 + } + + try await Task.sleep(nanoseconds: 400_000_000) // 0.4s + #expect(counter == 1) + } + + @Test("Only last call executes when called multiple times rapidly") + func onlyLastCallExecutes() async throws { + let debouncer = Debouncer(delay: 0.2) + var values: [Int] = [] + + for i in 1...5 { + debouncer.call { + values.append(i) + } + try await Task.sleep(nanoseconds: 50_000_000) // 0.05s + } + + try await Task.sleep(nanoseconds: 300_000_000) // wait for final call + #expect(values == [5]) + } + + @Test("Does not execute if delay has not elapsed") + func doesNotRunTooSoon() async throws { + let debouncer = Debouncer(delay: 0.5) + var didRun = false + + debouncer.call { + didRun = true + } + + try await Task.sleep(nanoseconds: 200_000_000) // 0.2s + #expect(didRun == false) + } +} diff --git a/Tests/EasyCoreTests/DecoderTests.swift b/Tests/EasyCoreTests/DecoderTests.swift new file mode 100644 index 0000000..207e479 --- /dev/null +++ b/Tests/EasyCoreTests/DecoderTests.swift @@ -0,0 +1,190 @@ +import Testing +import Foundation + +@testable import EasyCore + +@Suite("Decoder") +struct DecoderTests { + private struct User: Decodable, Equatable { + let name: String + let age: Int + } + + @Suite("Data.decode(_:decoder:)") + struct DataDecodeTests { + + @Test("Decodes valid JSON into expected model") + func validDecoding() { + let json = """ + { + "name": "Letícia", + "age": 28 + } + """ + let data = Data(json.utf8) + let user = data.decode(User.self) + + #expect(user == User(name: "Letícia", age: 28)) + } + + @Test("Returns nil when decoding invalid JSON") + func invalidJSON() { + let json = """ + { + "name": "Letícia" + "age": 28 + } + """ // missing comma + let data = Data(json.utf8) + let user = data.decode(User.self) + + #expect(user == nil) + } + + @Test("Returns nil when structure doesn't match expected model") + func mismatchedStructure() { + let json = """ + { + "username": "Letícia", + "yearsOld": 28 + } + """ + let data = Data(json.utf8) + let user = data.decode(User.self) + + #expect(user == nil) + } + + @Test("Supports decoding with custom decoder") + func customDecoder() { + struct DateModel: Decodable, Equatable { + let createdAt: Date + } + + let json = #"{"createdAt": "2023-10-15T12:00:00Z"}"# + let data = Data(json.utf8) + + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + + let result = data.decode(DateModel.self, decoder: decoder) + #expect(result?.createdAt == ISO8601DateFormatter().date(from: "2023-10-15T12:00:00Z")) + } + } + + @Suite("String.decode(_:decoder:)") + struct StringDecodeTests { + + @Test("Decodes a valid JSON string into a model") + func validJSONString() { + let json = """ + { + "name": "Letícia", + "age": 28 + } + """ + let result = json.decode(User.self) + #expect(result == User(name: "Letícia", age: 28)) + } + + @Test("Returns nil when string is not valid JSON") + func invalidJSONString() { + let json = """ + { + "name": "Letícia" + "age": 28 + } + """ // Missing comma + let result = json.decode(User.self) + #expect(result == nil) + } + + @Test("Returns nil when JSON structure does not match model") + func mismatchedStructure() { + let json = """ + { + "username": "Letícia", + "years": 28 + } + """ + let result = json.decode(User.self) + #expect(result == nil) + } + + @Test("Returns nil when string is not UTF-8 encodable") + func invalidEncoding() { + let string = "👩‍💻".applyingTransform(.toLatin, reverse: false) ?? "" // creates a non-UTF-8 safe string + let result = string.decode(User.self) + #expect(result == nil) + } + + @Test("Decodes with custom decoder strategy") + func customDateDecoder() { + struct Event: Decodable, Equatable { + let date: Date + } + + let json = #"{"date":"2023-05-01T14:30:00Z"}"# + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + + let result = json.decode(Event.self, decoder: decoder) + let expected = ISO8601DateFormatter().date(from: "2023-05-01T14:30:00Z") + #expect(result?.date == expected) + } + } + + @Suite("JSONDecoder Presets") + @MainActor + struct JSONDecoderPresetsTests { + + @Test("standard returns a fresh decoder instance") + func standardIsDefault() { + let decoder = JSONDecoder.standard + #expect(String(describing: decoder.keyDecodingStrategy) == String(describing: JSONDecoder.KeyDecodingStrategy.useDefaultKeys)) + #expect(String(describing: decoder.dateDecodingStrategy) == String(describing: JSONDecoder.DateDecodingStrategy.deferredToDate)) + } + + @Test("snakeCase applies convertFromSnakeCase strategy") + func snakeCaseDecoder() { + let decoder = JSONDecoder.snakeCase + + #expect(String(describing: decoder.keyDecodingStrategy) == String(describing: JSONDecoder.KeyDecodingStrategy.convertFromSnakeCase)) + } + + @Test("iso8601 applies ISO8601 date decoding strategy") + func iso8601Decoder() { + let decoder = JSONDecoder.iso8601 + + #expect(String(describing: decoder.dateDecodingStrategy) == String(describing: JSONDecoder.DateDecodingStrategy.iso8601)) + } + + @Test("secondsSince1970 applies secondsSince1970 date decoding strategy") + func secondsSince1970Decoder() { + let decoder = JSONDecoder.secondsSince1970 + #expect(String(describing: decoder.dateDecodingStrategy) == String(describing: JSONDecoder.DateDecodingStrategy.secondsSince1970)) + } + + @Test("formatted(using:) applies custom date formatter") + func formattedUsingCustomFormatter() { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd" + + let decoder = JSONDecoder.formatted(using: formatter) + #expect(String(describing: decoder.dateDecodingStrategy) == String(describing: JSONDecoder.DateDecodingStrategy.formatted(formatter))) + } + + @Test("Chained modifiers preserve strategy") + func chainingModifiersWorks() { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd" + + let decoder = JSONDecoder.standard + .snakeCase + .formatted(using: formatter) + + + #expect(String(describing: decoder.dateDecodingStrategy) == String(describing: JSONDecoder.DateDecodingStrategy.formatted(formatter))) + } + } +} diff --git a/Tests/EasyCoreTests/EasyCoreTests.swift b/Tests/EasyCoreTests/EasyCoreTests.swift deleted file mode 100644 index 499ea4e..0000000 --- a/Tests/EasyCoreTests/EasyCoreTests.swift +++ /dev/null @@ -1,6 +0,0 @@ -import Testing -@testable import EasyCore - -@Test func example() async throws { - // Write your test here and use APIs like `#expect(...)` to check expected conditions. -} diff --git a/Tests/EasyCoreTests/EncoderTests.swift b/Tests/EasyCoreTests/EncoderTests.swift new file mode 100644 index 0000000..f3d18ca --- /dev/null +++ b/Tests/EasyCoreTests/EncoderTests.swift @@ -0,0 +1,105 @@ +import Foundation +import Testing + +@testable import EasyCore + +@Suite("Encoder") +struct EncoderTests { + private struct User: Encodable, Equatable { + let name: String + let age: Int + } + + @Suite("Encodable extensions") + struct EncodableTests { + + @Test("Encodes object to Data successfully") + func encodeToData() { + let user = User(name: "Letícia", age: 28) + let data = user.encode() + + #expect(data != nil) + + let json = try? JSONSerialization.jsonObject(with: data!) + #expect(json is [String: Any]) + } + + @Test("Encodes object to JSON string") + func encodeToJSONString() { + let user = User(name: "Letícia", age: 28) + let json = user.encodeToString() + + #expect(json?.contains("\"name\"") == true) + #expect(json?.contains("\"Letícia\"") == true) + } + + @Test("Encodes object to pretty-printed JSON string") + func encodeToPrettyPrintedJSONString() { + let user = User(name: "Letícia", age: 28) + let json = user.encodeToString(prettyPrinted: true) + + #expect(json?.contains("\n") == true) + #expect(json?.contains(" ") == true) + } + + @Test("Converts object to dictionary representation") + func encodeToDictionary() { + let user = User(name: "Letícia", age: 28) + let dict = user.dictionary + + #expect(dict?["name"] as? String == "Letícia") + #expect(dict?["age"] as? Int == 28) + } + } + + @Suite("JSONEncoder Presets") + struct JSONEncoderPresetsTests { + + @Test("standard returns default configuration") + func standardReturnsDefaults() { + let encoder = JSONEncoder.standard + + #expect(String(describing: encoder.keyEncodingStrategy) == String(describing: JSONEncoder.KeyEncodingStrategy.useDefaultKeys)) + #expect(String(describing: encoder.dateEncodingStrategy) == String(describing: JSONEncoder.DateEncodingStrategy.deferredToDate)) + } + + @Test("snakeCase applies convertToSnakeCase") + func snakeCaseApplied() { + let encoder = JSONEncoder.snakeCase + #expect(String(describing: encoder.keyEncodingStrategy) == String(describing: JSONEncoder.KeyEncodingStrategy.convertToSnakeCase)) + } + + @Test("iso8601 applies ISO8601 date encoding strategy") + func iso8601Applied() { + let encoder = JSONEncoder.iso8601 + #expect(String(describing: encoder.dateEncodingStrategy) == String(describing: JSONEncoder.DateEncodingStrategy.iso8601)) + } + + @Test("secondsSince1970 applies secondsSince1970 date encoding strategy") + func secondsSince1970Applied() { + let encoder = JSONEncoder.secondsSince1970 + #expect(String(describing: encoder.dateEncodingStrategy) == String(describing: JSONEncoder.DateEncodingStrategy.secondsSince1970)) + } + + @Test("formatted(using:) applies custom DateFormatter strategy") + func formattedWithCustomDateFormatter() { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd" + let encoder = JSONEncoder.formatted(using: formatter) + #expect(String(describing: encoder.dateEncodingStrategy).contains("formatted")) + } + + @Test("Chaining snakeCase and formatted(using:) applies both strategies") + func chainingModifiersWorks() { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd" + + let encoder = JSONEncoder.standard + .snakeCase + .formatted(using: formatter) + + #expect(String(describing: encoder.keyEncodingStrategy) == String(describing: JSONEncoder.KeyEncodingStrategy.convertToSnakeCase)) + #expect(String(describing: encoder.dateEncodingStrategy).contains("formatted")) + } + } +} diff --git a/Tests/EasyCoreTests/LocaleExtensionTests.swift b/Tests/EasyCoreTests/LocaleExtensionTests.swift new file mode 100644 index 0000000..7dd2fba --- /dev/null +++ b/Tests/EasyCoreTests/LocaleExtensionTests.swift @@ -0,0 +1,453 @@ +import Foundation + +import Testing + +@testable import EasyCore + +@Suite("Locale") +struct Localetests { + + @Test("All locales") + func locales() { + #expect(Locale.aaDJ == LocaleIdentifier.aaDJ.locale!) + #expect(Locale.aaER == LocaleIdentifier.aaER.locale!) + #expect(Locale.aaET == LocaleIdentifier.aaET.locale!) + #expect(Locale.abGE == LocaleIdentifier.abGE.locale!) + #expect(Locale.afNA == LocaleIdentifier.afNA.locale!) + #expect(Locale.afZA == LocaleIdentifier.afZA.locale!) + #expect(Locale.akGH == LocaleIdentifier.akGH.locale!) + #expect(Locale.amET == LocaleIdentifier.amET.locale!) + #expect(Locale.anES == LocaleIdentifier.anES.locale!) + #expect(Locale.arAE == LocaleIdentifier.arAE.locale!) + #expect(Locale.arBH == LocaleIdentifier.arBH.locale!) + #expect(Locale.arDJ == LocaleIdentifier.arDJ.locale!) + #expect(Locale.arDZ == LocaleIdentifier.arDZ.locale!) + #expect(Locale.arEG == LocaleIdentifier.arEG.locale!) + #expect(Locale.arEH == LocaleIdentifier.arEH.locale!) + #expect(Locale.arER == LocaleIdentifier.arER.locale!) + #expect(Locale.arIL == LocaleIdentifier.arIL.locale!) + #expect(Locale.arIQ == LocaleIdentifier.arIQ.locale!) + #expect(Locale.arJO == LocaleIdentifier.arJO.locale!) + #expect(Locale.arKM == LocaleIdentifier.arKM.locale!) + #expect(Locale.arKW == LocaleIdentifier.arKW.locale!) + #expect(Locale.arLB == LocaleIdentifier.arLB.locale!) + #expect(Locale.arLY == LocaleIdentifier.arLY.locale!) + #expect(Locale.arMA == LocaleIdentifier.arMA.locale!) + #expect(Locale.arMR == LocaleIdentifier.arMR.locale!) + #expect(Locale.arOM == LocaleIdentifier.arOM.locale!) + #expect(Locale.arPS == LocaleIdentifier.arPS.locale!) + #expect(Locale.arQA == LocaleIdentifier.arQA.locale!) + #expect(Locale.arSA == LocaleIdentifier.arSA.locale!) + #expect(Locale.arSD == LocaleIdentifier.arSD.locale!) + #expect(Locale.arSO == LocaleIdentifier.arSO.locale!) + #expect(Locale.arSS == LocaleIdentifier.arSS.locale!) + #expect(Locale.arSY == LocaleIdentifier.arSY.locale!) + #expect(Locale.arTD == LocaleIdentifier.arTD.locale!) + #expect(Locale.arTN == LocaleIdentifier.arTN.locale!) + #expect(Locale.arYE == LocaleIdentifier.arYE.locale!) + #expect(Locale.asIN == LocaleIdentifier.asIN.locale!) + #expect(Locale.baRU == LocaleIdentifier.baRU.locale!) + #expect(Locale.beBY == LocaleIdentifier.beBY.locale!) + #expect(Locale.bgBG == LocaleIdentifier.bgBG.locale!) + #expect(Locale.bmML == LocaleIdentifier.bmML.locale!) + #expect(Locale.bnBD == LocaleIdentifier.bnBD.locale!) + #expect(Locale.bnIN == LocaleIdentifier.bnIN.locale!) + #expect(Locale.boCN == LocaleIdentifier.boCN.locale!) + #expect(Locale.boIN == LocaleIdentifier.boIN.locale!) + #expect(Locale.brFR == LocaleIdentifier.brFR.locale!) + #expect(Locale.caAD == LocaleIdentifier.caAD.locale!) + #expect(Locale.caES == LocaleIdentifier.caES.locale!) + #expect(Locale.caESVALENCIA == LocaleIdentifier.caESVALENCIA.locale!) + #expect(Locale.caFR == LocaleIdentifier.caFR.locale!) + #expect(Locale.caIT == LocaleIdentifier.caIT.locale!) + #expect(Locale.ceRU == LocaleIdentifier.ceRU.locale!) + #expect(Locale.coFR == LocaleIdentifier.coFR.locale!) + #expect(Locale.csCZ == LocaleIdentifier.csCZ.locale!) + #expect(Locale.cuRU == LocaleIdentifier.cuRU.locale!) + #expect(Locale.cvRU == LocaleIdentifier.cvRU.locale!) + #expect(Locale.cyGB == LocaleIdentifier.cyGB.locale!) + #expect(Locale.daDK == LocaleIdentifier.daDK.locale!) + #expect(Locale.daGL == LocaleIdentifier.daGL.locale!) + #expect(Locale.deAT == LocaleIdentifier.deAT.locale!) + #expect(Locale.deBE == LocaleIdentifier.deBE.locale!) + #expect(Locale.deCH == LocaleIdentifier.deCH.locale!) + #expect(Locale.deDE == LocaleIdentifier.deDE.locale!) + #expect(Locale.deIT == LocaleIdentifier.deIT.locale!) + #expect(Locale.deLI == LocaleIdentifier.deLI.locale!) + #expect(Locale.deLU == LocaleIdentifier.deLU.locale!) + #expect(Locale.dvMV == LocaleIdentifier.dvMV.locale!) + #expect(Locale.dzBT == LocaleIdentifier.dzBT.locale!) + #expect(Locale.eeGH == LocaleIdentifier.eeGH.locale!) + #expect(Locale.eeTG == LocaleIdentifier.eeTG.locale!) + #expect(Locale.elCY == LocaleIdentifier.elCY.locale!) + #expect(Locale.elGR == LocaleIdentifier.elGR.locale!) + #expect(Locale.enAE == LocaleIdentifier.enAE.locale!) + #expect(Locale.enAG == LocaleIdentifier.enAG.locale!) + #expect(Locale.enAI == LocaleIdentifier.enAI.locale!) + #expect(Locale.enAS == LocaleIdentifier.enAS.locale!) + #expect(Locale.enAT == LocaleIdentifier.enAT.locale!) + #expect(Locale.enAU == LocaleIdentifier.enAU.locale!) + #expect(Locale.enBB == LocaleIdentifier.enBB.locale!) + #expect(Locale.enBE == LocaleIdentifier.enBE.locale!) + #expect(Locale.enBI == LocaleIdentifier.enBI.locale!) + #expect(Locale.enBM == LocaleIdentifier.enBM.locale!) + #expect(Locale.enBS == LocaleIdentifier.enBS.locale!) + #expect(Locale.enBW == LocaleIdentifier.enBW.locale!) + #expect(Locale.enBZ == LocaleIdentifier.enBZ.locale!) + #expect(Locale.enCA == LocaleIdentifier.enCA.locale!) + #expect(Locale.enCC == LocaleIdentifier.enCC.locale!) + #expect(Locale.enCH == LocaleIdentifier.enCH.locale!) + #expect(Locale.enCK == LocaleIdentifier.enCK.locale!) + #expect(Locale.enCM == LocaleIdentifier.enCM.locale!) + #expect(Locale.enCX == LocaleIdentifier.enCX.locale!) + #expect(Locale.enCY == LocaleIdentifier.enCY.locale!) + #expect(Locale.enDE == LocaleIdentifier.enDE.locale!) + #expect(Locale.enDG == LocaleIdentifier.enDG.locale!) + #expect(Locale.enDK == LocaleIdentifier.enDK.locale!) + #expect(Locale.enDM == LocaleIdentifier.enDM.locale!) + #expect(Locale.enER == LocaleIdentifier.enER.locale!) + #expect(Locale.enFI == LocaleIdentifier.enFI.locale!) + #expect(Locale.enFJ == LocaleIdentifier.enFJ.locale!) + #expect(Locale.enFK == LocaleIdentifier.enFK.locale!) + #expect(Locale.enFM == LocaleIdentifier.enFM.locale!) + #expect(Locale.enGB == LocaleIdentifier.enGB.locale!) + #expect(Locale.enGD == LocaleIdentifier.enGD.locale!) + #expect(Locale.enGG == LocaleIdentifier.enGG.locale!) + #expect(Locale.enGH == LocaleIdentifier.enGH.locale!) + #expect(Locale.enGI == LocaleIdentifier.enGI.locale!) + #expect(Locale.enGM == LocaleIdentifier.enGM.locale!) + #expect(Locale.enGU == LocaleIdentifier.enGU.locale!) + #expect(Locale.enGY == LocaleIdentifier.enGY.locale!) + #expect(Locale.enHK == LocaleIdentifier.enHK.locale!) + #expect(Locale.enID == LocaleIdentifier.enID.locale!) + #expect(Locale.enIE == LocaleIdentifier.enIE.locale!) + #expect(Locale.enIL == LocaleIdentifier.enIL.locale!) + #expect(Locale.enIM == LocaleIdentifier.enIM.locale!) + #expect(Locale.enIN == LocaleIdentifier.enIN.locale!) + #expect(Locale.enIO == LocaleIdentifier.enIO.locale!) + #expect(Locale.enJE == LocaleIdentifier.enJE.locale!) + #expect(Locale.enJM == LocaleIdentifier.enJM.locale!) + #expect(Locale.enKE == LocaleIdentifier.enKE.locale!) + #expect(Locale.enKI == LocaleIdentifier.enKI.locale!) + #expect(Locale.enKN == LocaleIdentifier.enKN.locale!) + #expect(Locale.enKY == LocaleIdentifier.enKY.locale!) + #expect(Locale.enLC == LocaleIdentifier.enLC.locale!) + #expect(Locale.enLR == LocaleIdentifier.enLR.locale!) + #expect(Locale.enLS == LocaleIdentifier.enLS.locale!) + #expect(Locale.enMG == LocaleIdentifier.enMG.locale!) + #expect(Locale.enMH == LocaleIdentifier.enMH.locale!) + #expect(Locale.enMO == LocaleIdentifier.enMO.locale!) + #expect(Locale.enMP == LocaleIdentifier.enMP.locale!) + #expect(Locale.enMS == LocaleIdentifier.enMS.locale!) + #expect(Locale.enMT == LocaleIdentifier.enMT.locale!) + #expect(Locale.enMU == LocaleIdentifier.enMU.locale!) + #expect(Locale.enMV == LocaleIdentifier.enMV.locale!) + #expect(Locale.enMW == LocaleIdentifier.enMW.locale!) + #expect(Locale.enMY == LocaleIdentifier.enMY.locale!) + #expect(Locale.enNA == LocaleIdentifier.enNA.locale!) + #expect(Locale.enNF == LocaleIdentifier.enNF.locale!) + #expect(Locale.enNG == LocaleIdentifier.enNG.locale!) + #expect(Locale.enNL == LocaleIdentifier.enNL.locale!) + #expect(Locale.enNR == LocaleIdentifier.enNR.locale!) + #expect(Locale.enNU == LocaleIdentifier.enNU.locale!) + #expect(Locale.enNZ == LocaleIdentifier.enNZ.locale!) + #expect(Locale.enPG == LocaleIdentifier.enPG.locale!) + #expect(Locale.enPH == LocaleIdentifier.enPH.locale!) + #expect(Locale.enPK == LocaleIdentifier.enPK.locale!) + #expect(Locale.enPN == LocaleIdentifier.enPN.locale!) + #expect(Locale.enPR == LocaleIdentifier.enPR.locale!) + #expect(Locale.enPW == LocaleIdentifier.enPW.locale!) + #expect(Locale.enRW == LocaleIdentifier.enRW.locale!) + #expect(Locale.enSB == LocaleIdentifier.enSB.locale!) + #expect(Locale.enSC == LocaleIdentifier.enSC.locale!) + #expect(Locale.enSD == LocaleIdentifier.enSD.locale!) + #expect(Locale.enSE == LocaleIdentifier.enSE.locale!) + #expect(Locale.enSG == LocaleIdentifier.enSG.locale!) + #expect(Locale.enSH == LocaleIdentifier.enSH.locale!) + #expect(Locale.enSI == LocaleIdentifier.enSI.locale!) + #expect(Locale.enSL == LocaleIdentifier.enSL.locale!) + #expect(Locale.enSS == LocaleIdentifier.enSS.locale!) + #expect(Locale.enSX == LocaleIdentifier.enSX.locale!) + #expect(Locale.enSZ == LocaleIdentifier.enSZ.locale!) + #expect(Locale.enTC == LocaleIdentifier.enTC.locale!) + #expect(Locale.enTK == LocaleIdentifier.enTK.locale!) + #expect(Locale.enTO == LocaleIdentifier.enTO.locale!) + #expect(Locale.enTT == LocaleIdentifier.enTT.locale!) + #expect(Locale.enTV == LocaleIdentifier.enTV.locale!) + #expect(Locale.enTZ == LocaleIdentifier.enTZ.locale!) + #expect(Locale.enUG == LocaleIdentifier.enUG.locale!) + #expect(Locale.enUM == LocaleIdentifier.enUM.locale!) + #expect(Locale.enUS == LocaleIdentifier.enUS.locale!) + #expect(Locale.enUSPOSIX == LocaleIdentifier.enUSPOSIX.locale!) + #expect(Locale.enVC == LocaleIdentifier.enVC.locale!) + #expect(Locale.enVG == LocaleIdentifier.enVG.locale!) + #expect(Locale.enVI == LocaleIdentifier.enVI.locale!) + #expect(Locale.enVU == LocaleIdentifier.enVU.locale!) + #expect(Locale.enWS == LocaleIdentifier.enWS.locale!) + #expect(Locale.enZA == LocaleIdentifier.enZA.locale!) + #expect(Locale.enZM == LocaleIdentifier.enZM.locale!) + #expect(Locale.enZW == LocaleIdentifier.enZW.locale!) + #expect(Locale.esAR == LocaleIdentifier.esAR.locale!) + #expect(Locale.esBO == LocaleIdentifier.esBO.locale!) + #expect(Locale.esBR == LocaleIdentifier.esBR.locale!) + #expect(Locale.esBZ == LocaleIdentifier.esBZ.locale!) + #expect(Locale.esCL == LocaleIdentifier.esCL.locale!) + #expect(Locale.esCO == LocaleIdentifier.esCO.locale!) + #expect(Locale.esCR == LocaleIdentifier.esCR.locale!) + #expect(Locale.esCU == LocaleIdentifier.esCU.locale!) + #expect(Locale.esDO == LocaleIdentifier.esDO.locale!) + #expect(Locale.esEA == LocaleIdentifier.esEA.locale!) + #expect(Locale.esEC == LocaleIdentifier.esEC.locale!) + #expect(Locale.esES == LocaleIdentifier.esES.locale!) + #expect(Locale.esGQ == LocaleIdentifier.esGQ.locale!) + #expect(Locale.esGT == LocaleIdentifier.esGT.locale!) + #expect(Locale.esHN == LocaleIdentifier.esHN.locale!) + #expect(Locale.esIC == LocaleIdentifier.esIC.locale!) + #expect(Locale.esMX == LocaleIdentifier.esMX.locale!) + #expect(Locale.esNI == LocaleIdentifier.esNI.locale!) + #expect(Locale.esPA == LocaleIdentifier.esPA.locale!) + #expect(Locale.esPE == LocaleIdentifier.esPE.locale!) + #expect(Locale.esPH == LocaleIdentifier.esPH.locale!) + #expect(Locale.esPR == LocaleIdentifier.esPR.locale!) + #expect(Locale.esPY == LocaleIdentifier.esPY.locale!) + #expect(Locale.esSV == LocaleIdentifier.esSV.locale!) + #expect(Locale.esUS == LocaleIdentifier.esUS.locale!) + #expect(Locale.esUY == LocaleIdentifier.esUY.locale!) + #expect(Locale.esVE == LocaleIdentifier.esVE.locale!) + #expect(Locale.etEE == LocaleIdentifier.etEE.locale!) + #expect(Locale.euES == LocaleIdentifier.euES.locale!) + #expect(Locale.faAF == LocaleIdentifier.faAF.locale!) + #expect(Locale.faIR == LocaleIdentifier.faIR.locale!) + #expect(Locale.fiFI == LocaleIdentifier.fiFI.locale!) + #expect(Locale.foDK == LocaleIdentifier.foDK.locale!) + #expect(Locale.foFO == LocaleIdentifier.foFO.locale!) + #expect(Locale.frBE == LocaleIdentifier.frBE.locale!) + #expect(Locale.frBF == LocaleIdentifier.frBF.locale!) + #expect(Locale.frBI == LocaleIdentifier.frBI.locale!) + #expect(Locale.frBJ == LocaleIdentifier.frBJ.locale!) + #expect(Locale.frBL == LocaleIdentifier.frBL.locale!) + #expect(Locale.frCA == LocaleIdentifier.frCA.locale!) + #expect(Locale.frCD == LocaleIdentifier.frCD.locale!) + #expect(Locale.frCF == LocaleIdentifier.frCF.locale!) + #expect(Locale.frCG == LocaleIdentifier.frCG.locale!) + #expect(Locale.frCH == LocaleIdentifier.frCH.locale!) + #expect(Locale.frCI == LocaleIdentifier.frCI.locale!) + #expect(Locale.frCM == LocaleIdentifier.frCM.locale!) + #expect(Locale.frDJ == LocaleIdentifier.frDJ.locale!) + #expect(Locale.frDZ == LocaleIdentifier.frDZ.locale!) + #expect(Locale.frFR == LocaleIdentifier.frFR.locale!) + #expect(Locale.frGA == LocaleIdentifier.frGA.locale!) + #expect(Locale.frGF == LocaleIdentifier.frGF.locale!) + #expect(Locale.frGN == LocaleIdentifier.frGN.locale!) + #expect(Locale.frGP == LocaleIdentifier.frGP.locale!) + #expect(Locale.frGQ == LocaleIdentifier.frGQ.locale!) + #expect(Locale.frHT == LocaleIdentifier.frHT.locale!) + #expect(Locale.frKM == LocaleIdentifier.frKM.locale!) + #expect(Locale.frLU == LocaleIdentifier.frLU.locale!) + #expect(Locale.frMA == LocaleIdentifier.frMA.locale!) + #expect(Locale.frMC == LocaleIdentifier.frMC.locale!) + #expect(Locale.frMF == LocaleIdentifier.frMF.locale!) + #expect(Locale.frMG == LocaleIdentifier.frMG.locale!) + #expect(Locale.frML == LocaleIdentifier.frML.locale!) + #expect(Locale.frMQ == LocaleIdentifier.frMQ.locale!) + #expect(Locale.frMR == LocaleIdentifier.frMR.locale!) + #expect(Locale.frMU == LocaleIdentifier.frMU.locale!) + #expect(Locale.frNC == LocaleIdentifier.frNC.locale!) + #expect(Locale.frNE == LocaleIdentifier.frNE.locale!) + #expect(Locale.frPF == LocaleIdentifier.frPF.locale!) + #expect(Locale.frPM == LocaleIdentifier.frPM.locale!) + #expect(Locale.frRE == LocaleIdentifier.frRE.locale!) + #expect(Locale.frRW == LocaleIdentifier.frRW.locale!) + #expect(Locale.frSC == LocaleIdentifier.frSC.locale!) + #expect(Locale.frSN == LocaleIdentifier.frSN.locale!) + #expect(Locale.frSY == LocaleIdentifier.frSY.locale!) + #expect(Locale.frTD == LocaleIdentifier.frTD.locale!) + #expect(Locale.frTG == LocaleIdentifier.frTG.locale!) + #expect(Locale.frTN == LocaleIdentifier.frTN.locale!) + #expect(Locale.frVU == LocaleIdentifier.frVU.locale!) + #expect(Locale.frWF == LocaleIdentifier.frWF.locale!) + #expect(Locale.frYT == LocaleIdentifier.frYT.locale!) + #expect(Locale.fyNL == LocaleIdentifier.fyNL.locale!) + #expect(Locale.gaGB == LocaleIdentifier.gaGB.locale!) + #expect(Locale.gaIE == LocaleIdentifier.gaIE.locale!) + #expect(Locale.gdGB == LocaleIdentifier.gdGB.locale!) + #expect(Locale.glES == LocaleIdentifier.glES.locale!) + #expect(Locale.gnPY == LocaleIdentifier.gnPY.locale!) + #expect(Locale.guIN == LocaleIdentifier.guIN.locale!) + #expect(Locale.gvIM == LocaleIdentifier.gvIM.locale!) + #expect(Locale.haGH == LocaleIdentifier.haGH.locale!) + #expect(Locale.haNE == LocaleIdentifier.haNE.locale!) + #expect(Locale.haNG == LocaleIdentifier.haNG.locale!) + #expect(Locale.heIL == LocaleIdentifier.heIL.locale!) + #expect(Locale.hiIN == LocaleIdentifier.hiIN.locale!) + #expect(Locale.hrBA == LocaleIdentifier.hrBA.locale!) + #expect(Locale.hrHR == LocaleIdentifier.hrHR.locale!) + #expect(Locale.huHU == LocaleIdentifier.huHU.locale!) + #expect(Locale.hyAM == LocaleIdentifier.hyAM.locale!) + #expect(Locale.idID == LocaleIdentifier.idID.locale!) + #expect(Locale.ieEE == LocaleIdentifier.ieEE.locale!) + #expect(Locale.igNG == LocaleIdentifier.igNG.locale!) + #expect(Locale.iiCN == LocaleIdentifier.iiCN.locale!) + #expect(Locale.isIS == LocaleIdentifier.isIS.locale!) + #expect(Locale.itCH == LocaleIdentifier.itCH.locale!) + #expect(Locale.itIT == LocaleIdentifier.itIT.locale!) + #expect(Locale.itSM == LocaleIdentifier.itSM.locale!) + #expect(Locale.itVA == LocaleIdentifier.itVA.locale!) + #expect(Locale.iuCA == LocaleIdentifier.iuCA.locale!) + #expect(Locale.jaJP == LocaleIdentifier.jaJP.locale!) + #expect(Locale.jvID == LocaleIdentifier.jvID.locale!) + #expect(Locale.kaGE == LocaleIdentifier.kaGE.locale!) + #expect(Locale.kiKE == LocaleIdentifier.kiKE.locale!) + #expect(Locale.kkKZ == LocaleIdentifier.kkKZ.locale!) + #expect(Locale.klGL == LocaleIdentifier.klGL.locale!) + #expect(Locale.kmKH == LocaleIdentifier.kmKH.locale!) + #expect(Locale.knIN == LocaleIdentifier.knIN.locale!) + #expect(Locale.koCN == LocaleIdentifier.koCN.locale!) + #expect(Locale.koKP == LocaleIdentifier.koKP.locale!) + #expect(Locale.koKR == LocaleIdentifier.koKR.locale!) + #expect(Locale.kuTR == LocaleIdentifier.kuTR.locale!) + #expect(Locale.kwGB == LocaleIdentifier.kwGB.locale!) + #expect(Locale.kyKG == LocaleIdentifier.kyKG.locale!) + #expect(Locale.laVA == LocaleIdentifier.laVA.locale!) + #expect(Locale.lbLU == LocaleIdentifier.lbLU.locale!) + #expect(Locale.lgUG == LocaleIdentifier.lgUG.locale!) + #expect(Locale.lnAO == LocaleIdentifier.lnAO.locale!) + #expect(Locale.lnCD == LocaleIdentifier.lnCD.locale!) + #expect(Locale.lnCF == LocaleIdentifier.lnCF.locale!) + #expect(Locale.lnCG == LocaleIdentifier.lnCG.locale!) + #expect(Locale.loLA == LocaleIdentifier.loLA.locale!) + #expect(Locale.ltLT == LocaleIdentifier.ltLT.locale!) + #expect(Locale.luCD == LocaleIdentifier.luCD.locale!) + #expect(Locale.lvLV == LocaleIdentifier.lvLV.locale!) + #expect(Locale.mgMG == LocaleIdentifier.mgMG.locale!) + #expect(Locale.miNZ == LocaleIdentifier.miNZ.locale!) + #expect(Locale.mkMK == LocaleIdentifier.mkMK.locale!) + #expect(Locale.mlIN == LocaleIdentifier.mlIN.locale!) + #expect(Locale.mnMN == LocaleIdentifier.mnMN.locale!) + #expect(Locale.mrIN == LocaleIdentifier.mrIN.locale!) + #expect(Locale.msBN == LocaleIdentifier.msBN.locale!) + #expect(Locale.msID == LocaleIdentifier.msID.locale!) + #expect(Locale.msMY == LocaleIdentifier.msMY.locale!) + #expect(Locale.msSG == LocaleIdentifier.msSG.locale!) + #expect(Locale.mtMT == LocaleIdentifier.mtMT.locale!) + #expect(Locale.myMM == LocaleIdentifier.myMM.locale!) + #expect(Locale.nbNO == LocaleIdentifier.nbNO.locale!) + #expect(Locale.nbSJ == LocaleIdentifier.nbSJ.locale!) + #expect(Locale.ndZW == LocaleIdentifier.ndZW.locale!) + #expect(Locale.neIN == LocaleIdentifier.neIN.locale!) + #expect(Locale.neNP == LocaleIdentifier.neNP.locale!) + #expect(Locale.nlAW == LocaleIdentifier.nlAW.locale!) + #expect(Locale.nlBE == LocaleIdentifier.nlBE.locale!) + #expect(Locale.nlBQ == LocaleIdentifier.nlBQ.locale!) + #expect(Locale.nlCW == LocaleIdentifier.nlCW.locale!) + #expect(Locale.nlNL == LocaleIdentifier.nlNL.locale!) + #expect(Locale.nlSR == LocaleIdentifier.nlSR.locale!) + #expect(Locale.nlSX == LocaleIdentifier.nlSX.locale!) + #expect(Locale.nnNO == LocaleIdentifier.nnNO.locale!) + #expect(Locale.nrZA == LocaleIdentifier.nrZA.locale!) + #expect(Locale.nvUS == LocaleIdentifier.nvUS.locale!) + #expect(Locale.nyMW == LocaleIdentifier.nyMW.locale!) + #expect(Locale.ocES == LocaleIdentifier.ocES.locale!) + #expect(Locale.ocFR == LocaleIdentifier.ocFR.locale!) + #expect(Locale.omET == LocaleIdentifier.omET.locale!) + #expect(Locale.omKE == LocaleIdentifier.omKE.locale!) + #expect(Locale.orIN == LocaleIdentifier.orIN.locale!) + #expect(Locale.osGE == LocaleIdentifier.osGE.locale!) + #expect(Locale.osRU == LocaleIdentifier.osRU.locale!) + #expect(Locale.plPL == LocaleIdentifier.plPL.locale!) + #expect(Locale.psAF == LocaleIdentifier.psAF.locale!) + #expect(Locale.psPK == LocaleIdentifier.psPK.locale!) + #expect(Locale.ptAO == LocaleIdentifier.ptAO.locale!) + #expect(Locale.ptBR == LocaleIdentifier.ptBR.locale!) + #expect(Locale.ptCH == LocaleIdentifier.ptCH.locale!) + #expect(Locale.ptCV == LocaleIdentifier.ptCV.locale!) + #expect(Locale.ptGQ == LocaleIdentifier.ptGQ.locale!) + #expect(Locale.ptGW == LocaleIdentifier.ptGW.locale!) + #expect(Locale.ptLU == LocaleIdentifier.ptLU.locale!) + #expect(Locale.ptMO == LocaleIdentifier.ptMO.locale!) + #expect(Locale.ptMZ == LocaleIdentifier.ptMZ.locale!) + #expect(Locale.ptPT == LocaleIdentifier.ptPT.locale!) + #expect(Locale.ptST == LocaleIdentifier.ptST.locale!) + #expect(Locale.ptTL == LocaleIdentifier.ptTL.locale!) + #expect(Locale.quBO == LocaleIdentifier.quBO.locale!) + #expect(Locale.quEC == LocaleIdentifier.quEC.locale!) + #expect(Locale.quPE == LocaleIdentifier.quPE.locale!) + #expect(Locale.rmCH == LocaleIdentifier.rmCH.locale!) + #expect(Locale.rnBI == LocaleIdentifier.rnBI.locale!) + #expect(Locale.roMD == LocaleIdentifier.roMD.locale!) + #expect(Locale.roRO == LocaleIdentifier.roRO.locale!) + #expect(Locale.ruBY == LocaleIdentifier.ruBY.locale!) + #expect(Locale.ruKG == LocaleIdentifier.ruKG.locale!) + #expect(Locale.ruKZ == LocaleIdentifier.ruKZ.locale!) + #expect(Locale.ruMD == LocaleIdentifier.ruMD.locale!) + #expect(Locale.ruRU == LocaleIdentifier.ruRU.locale!) + #expect(Locale.ruUA == LocaleIdentifier.ruUA.locale!) + #expect(Locale.rwRW == LocaleIdentifier.rwRW.locale!) + #expect(Locale.saIN == LocaleIdentifier.saIN.locale!) + #expect(Locale.scIT == LocaleIdentifier.scIT.locale!) + #expect(Locale.seFI == LocaleIdentifier.seFI.locale!) + #expect(Locale.seNO == LocaleIdentifier.seNO.locale!) + #expect(Locale.seSE == LocaleIdentifier.seSE.locale!) + #expect(Locale.sgCF == LocaleIdentifier.sgCF.locale!) + #expect(Locale.siLK == LocaleIdentifier.siLK.locale!) + #expect(Locale.skSK == LocaleIdentifier.skSK.locale!) + #expect(Locale.slSI == LocaleIdentifier.slSI.locale!) + #expect(Locale.snZW == LocaleIdentifier.snZW.locale!) + #expect(Locale.soDJ == LocaleIdentifier.soDJ.locale!) + #expect(Locale.soET == LocaleIdentifier.soET.locale!) + #expect(Locale.soKE == LocaleIdentifier.soKE.locale!) + #expect(Locale.soSO == LocaleIdentifier.soSO.locale!) + #expect(Locale.sqAL == LocaleIdentifier.sqAL.locale!) + #expect(Locale.sqMK == LocaleIdentifier.sqMK.locale!) + #expect(Locale.sqXK == LocaleIdentifier.sqXK.locale!) + #expect(Locale.ssSZ == LocaleIdentifier.ssSZ.locale!) + #expect(Locale.ssZA == LocaleIdentifier.ssZA.locale!) + #expect(Locale.stLS == LocaleIdentifier.stLS.locale!) + #expect(Locale.stZA == LocaleIdentifier.stZA.locale!) + #expect(Locale.svAX == LocaleIdentifier.svAX.locale!) + #expect(Locale.svFI == LocaleIdentifier.svFI.locale!) + #expect(Locale.svSE == LocaleIdentifier.svSE.locale!) + #expect(Locale.swCD == LocaleIdentifier.swCD.locale!) + #expect(Locale.swKE == LocaleIdentifier.swKE.locale!) + #expect(Locale.swTZ == LocaleIdentifier.swTZ.locale!) + #expect(Locale.swUG == LocaleIdentifier.swUG.locale!) + #expect(Locale.taIN == LocaleIdentifier.taIN.locale!) + #expect(Locale.taLK == LocaleIdentifier.taLK.locale!) + #expect(Locale.taMY == LocaleIdentifier.taMY.locale!) + #expect(Locale.taSG == LocaleIdentifier.taSG.locale!) + #expect(Locale.teIN == LocaleIdentifier.teIN.locale!) + #expect(Locale.tgTJ == LocaleIdentifier.tgTJ.locale!) + #expect(Locale.thTH == LocaleIdentifier.thTH.locale!) + #expect(Locale.tiER == LocaleIdentifier.tiER.locale!) + #expect(Locale.tiET == LocaleIdentifier.tiET.locale!) + #expect(Locale.tkTM == LocaleIdentifier.tkTM.locale!) + #expect(Locale.tnBW == LocaleIdentifier.tnBW.locale!) + #expect(Locale.tnZA == LocaleIdentifier.tnZA.locale!) + #expect(Locale.toTO == LocaleIdentifier.toTO.locale!) + #expect(Locale.trCY == LocaleIdentifier.trCY.locale!) + #expect(Locale.trTR == LocaleIdentifier.trTR.locale!) + #expect(Locale.tsZA == LocaleIdentifier.tsZA.locale!) + #expect(Locale.ttRU == LocaleIdentifier.ttRU.locale!) + #expect(Locale.ugCN == LocaleIdentifier.ugCN.locale!) + #expect(Locale.ukUA == LocaleIdentifier.ukUA.locale!) + #expect(Locale.urIN == LocaleIdentifier.urIN.locale!) + #expect(Locale.urPK == LocaleIdentifier.urPK.locale!) + #expect(Locale.veZA == LocaleIdentifier.veZA.locale!) + #expect(Locale.viVN == LocaleIdentifier.viVN.locale!) + #expect(Locale.waBE == LocaleIdentifier.waBE.locale!) + #expect(Locale.woSN == LocaleIdentifier.woSN.locale!) + #expect(Locale.xhZA == LocaleIdentifier.xhZA.locale!) + #expect(Locale.yiUA == LocaleIdentifier.yiUA.locale!) + #expect(Locale.yoBJ == LocaleIdentifier.yoBJ.locale!) + #expect(Locale.yoNG == LocaleIdentifier.yoNG.locale!) + #expect(Locale.zaCN == LocaleIdentifier.zaCN.locale!) + #expect(Locale.zuZA == LocaleIdentifier.zuZA.locale!) + #expect(Locale.zhCN == LocaleIdentifier.zhCN.locale!) + #expect(Locale.zhTW == LocaleIdentifier.zhTW.locale!) + #expect(Locale.paIN == LocaleIdentifier.paIN.locale!) + #expect(Locale.azAZ == LocaleIdentifier.azAZ.locale!) + #expect(Locale.suID == LocaleIdentifier.suID.locale!) + #expect(Locale.cebPH == LocaleIdentifier.cebPH.locale!) + #expect(Locale.srRS == LocaleIdentifier.srRS.locale!) + } +} diff --git a/Tests/EasyCoreTests/LocaleIdentifierTests.swift b/Tests/EasyCoreTests/LocaleIdentifierTests.swift new file mode 100644 index 0000000..306cab5 --- /dev/null +++ b/Tests/EasyCoreTests/LocaleIdentifierTests.swift @@ -0,0 +1,451 @@ +import Testing + +@testable import EasyCore + +@Suite("LocaleIdentifier rawValues") +struct LocaleIdentifierRawValueTests { + + @Test("All rawValues match expected strings") + func rawValuesMatchExpected() { + #expect(LocaleIdentifier.aaDJ.rawValue == "aa_DJ") + #expect(LocaleIdentifier.aaER.rawValue == "aa_ER") + #expect(LocaleIdentifier.aaET.rawValue == "aa_ET") + #expect(LocaleIdentifier.abGE.rawValue == "ab_GE") + #expect(LocaleIdentifier.afNA.rawValue == "af_NA") + #expect(LocaleIdentifier.afZA.rawValue == "af_ZA") + #expect(LocaleIdentifier.akGH.rawValue == "ak_GH") + #expect(LocaleIdentifier.amET.rawValue == "am_ET") + #expect(LocaleIdentifier.anES.rawValue == "an_ES") + #expect(LocaleIdentifier.arAE.rawValue == "ar_AE") + #expect(LocaleIdentifier.arBH.rawValue == "ar_BH") + #expect(LocaleIdentifier.arDJ.rawValue == "ar_DJ") + #expect(LocaleIdentifier.arDZ.rawValue == "ar_DZ") + #expect(LocaleIdentifier.arEG.rawValue == "ar_EG") + #expect(LocaleIdentifier.arEH.rawValue == "ar_EH") + #expect(LocaleIdentifier.arER.rawValue == "ar_ER") + #expect(LocaleIdentifier.arIL.rawValue == "ar_IL") + #expect(LocaleIdentifier.arIQ.rawValue == "ar_IQ") + #expect(LocaleIdentifier.arJO.rawValue == "ar_JO") + #expect(LocaleIdentifier.arKM.rawValue == "ar_KM") + #expect(LocaleIdentifier.arKW.rawValue == "ar_KW") + #expect(LocaleIdentifier.arLB.rawValue == "ar_LB") + #expect(LocaleIdentifier.arLY.rawValue == "ar_LY") + #expect(LocaleIdentifier.arMA.rawValue == "ar_MA") + #expect(LocaleIdentifier.arMR.rawValue == "ar_MR") + #expect(LocaleIdentifier.arOM.rawValue == "ar_OM") + #expect(LocaleIdentifier.arPS.rawValue == "ar_PS") + #expect(LocaleIdentifier.arQA.rawValue == "ar_QA") + #expect(LocaleIdentifier.arSA.rawValue == "ar_SA") + #expect(LocaleIdentifier.arSD.rawValue == "ar_SD") + #expect(LocaleIdentifier.arSO.rawValue == "ar_SO") + #expect(LocaleIdentifier.arSS.rawValue == "ar_SS") + #expect(LocaleIdentifier.arSY.rawValue == "ar_SY") + #expect(LocaleIdentifier.arTD.rawValue == "ar_TD") + #expect(LocaleIdentifier.arTN.rawValue == "ar_TN") + #expect(LocaleIdentifier.arYE.rawValue == "ar_YE") + #expect(LocaleIdentifier.asIN.rawValue == "as_IN") + #expect(LocaleIdentifier.baRU.rawValue == "ba_RU") + #expect(LocaleIdentifier.beBY.rawValue == "be_BY") + #expect(LocaleIdentifier.bgBG.rawValue == "bg_BG") + #expect(LocaleIdentifier.bmML.rawValue == "bm_ML") + #expect(LocaleIdentifier.bnBD.rawValue == "bn_BD") + #expect(LocaleIdentifier.bnIN.rawValue == "bn_IN") + #expect(LocaleIdentifier.boCN.rawValue == "bo_CN") + #expect(LocaleIdentifier.boIN.rawValue == "bo_IN") + #expect(LocaleIdentifier.brFR.rawValue == "br_FR") + #expect(LocaleIdentifier.caAD.rawValue == "ca_AD") + #expect(LocaleIdentifier.caES.rawValue == "ca_ES") + #expect(LocaleIdentifier.caESVALENCIA.rawValue == "ca_ES_VALENCIA") + #expect(LocaleIdentifier.caFR.rawValue == "ca_FR") + #expect(LocaleIdentifier.caIT.rawValue == "ca_IT") + #expect(LocaleIdentifier.ceRU.rawValue == "ce_RU") + #expect(LocaleIdentifier.coFR.rawValue == "co_FR") + #expect(LocaleIdentifier.csCZ.rawValue == "cs_CZ") + #expect(LocaleIdentifier.cuRU.rawValue == "cu_RU") + #expect(LocaleIdentifier.cvRU.rawValue == "cv_RU") + #expect(LocaleIdentifier.cyGB.rawValue == "cy_GB") + #expect(LocaleIdentifier.daDK.rawValue == "da_DK") + #expect(LocaleIdentifier.daGL.rawValue == "da_GL") + #expect(LocaleIdentifier.deAT.rawValue == "de_AT") + #expect(LocaleIdentifier.deBE.rawValue == "de_BE") + #expect(LocaleIdentifier.deCH.rawValue == "de_CH") + #expect(LocaleIdentifier.deDE.rawValue == "de_DE") + #expect(LocaleIdentifier.deIT.rawValue == "de_IT") + #expect(LocaleIdentifier.deLI.rawValue == "de_LI") + #expect(LocaleIdentifier.deLU.rawValue == "de_LU") + #expect(LocaleIdentifier.dvMV.rawValue == "dv_MV") + #expect(LocaleIdentifier.dzBT.rawValue == "dz_BT") + #expect(LocaleIdentifier.eeGH.rawValue == "ee_GH") + #expect(LocaleIdentifier.eeTG.rawValue == "ee_TG") + #expect(LocaleIdentifier.elCY.rawValue == "el_CY") + #expect(LocaleIdentifier.elGR.rawValue == "el_GR") + #expect(LocaleIdentifier.enAE.rawValue == "en_AE") + #expect(LocaleIdentifier.enAG.rawValue == "en_AG") + #expect(LocaleIdentifier.enAI.rawValue == "en_AI") + #expect(LocaleIdentifier.enAS.rawValue == "en_AS") + #expect(LocaleIdentifier.enAT.rawValue == "en_AT") + #expect(LocaleIdentifier.enAU.rawValue == "en_AU") + #expect(LocaleIdentifier.enBB.rawValue == "en_BB") + #expect(LocaleIdentifier.enBE.rawValue == "en_BE") + #expect(LocaleIdentifier.enBI.rawValue == "en_BI") + #expect(LocaleIdentifier.enBM.rawValue == "en_BM") + #expect(LocaleIdentifier.enBS.rawValue == "en_BS") + #expect(LocaleIdentifier.enBW.rawValue == "en_BW") + #expect(LocaleIdentifier.enBZ.rawValue == "en_BZ") + #expect(LocaleIdentifier.enCA.rawValue == "en_CA") + #expect(LocaleIdentifier.enCC.rawValue == "en_CC") + #expect(LocaleIdentifier.enCH.rawValue == "en_CH") + #expect(LocaleIdentifier.enCK.rawValue == "en_CK") + #expect(LocaleIdentifier.enCM.rawValue == "en_CM") + #expect(LocaleIdentifier.enCX.rawValue == "en_CX") + #expect(LocaleIdentifier.enCY.rawValue == "en_CY") + #expect(LocaleIdentifier.enDE.rawValue == "en_DE") + #expect(LocaleIdentifier.enDG.rawValue == "en_DG") + #expect(LocaleIdentifier.enDK.rawValue == "en_DK") + #expect(LocaleIdentifier.enDM.rawValue == "en_DM") + #expect(LocaleIdentifier.enER.rawValue == "en_ER") + #expect(LocaleIdentifier.enFI.rawValue == "en_FI") + #expect(LocaleIdentifier.enFJ.rawValue == "en_FJ") + #expect(LocaleIdentifier.enFK.rawValue == "en_FK") + #expect(LocaleIdentifier.enFM.rawValue == "en_FM") + #expect(LocaleIdentifier.enGB.rawValue == "en_GB") + #expect(LocaleIdentifier.enGD.rawValue == "en_GD") + #expect(LocaleIdentifier.enGG.rawValue == "en_GG") + #expect(LocaleIdentifier.enGH.rawValue == "en_GH") + #expect(LocaleIdentifier.enGI.rawValue == "en_GI") + #expect(LocaleIdentifier.enGM.rawValue == "en_GM") + #expect(LocaleIdentifier.enGU.rawValue == "en_GU") + #expect(LocaleIdentifier.enGY.rawValue == "en_GY") + #expect(LocaleIdentifier.enHK.rawValue == "en_HK") + #expect(LocaleIdentifier.enID.rawValue == "en_ID") + #expect(LocaleIdentifier.enIE.rawValue == "en_IE") + #expect(LocaleIdentifier.enIL.rawValue == "en_IL") + #expect(LocaleIdentifier.enIM.rawValue == "en_IM") + #expect(LocaleIdentifier.enIN.rawValue == "en_IN") + #expect(LocaleIdentifier.enIO.rawValue == "en_IO") + #expect(LocaleIdentifier.enJE.rawValue == "en_JE") + #expect(LocaleIdentifier.enJM.rawValue == "en_JM") + #expect(LocaleIdentifier.enKE.rawValue == "en_KE") + #expect(LocaleIdentifier.enKI.rawValue == "en_KI") + #expect(LocaleIdentifier.enKN.rawValue == "en_KN") + #expect(LocaleIdentifier.enKY.rawValue == "en_KY") + #expect(LocaleIdentifier.enLC.rawValue == "en_LC") + #expect(LocaleIdentifier.enLR.rawValue == "en_LR") + #expect(LocaleIdentifier.enLS.rawValue == "en_LS") + #expect(LocaleIdentifier.enMG.rawValue == "en_MG") + #expect(LocaleIdentifier.enMH.rawValue == "en_MH") + #expect(LocaleIdentifier.enMO.rawValue == "en_MO") + #expect(LocaleIdentifier.enMP.rawValue == "en_MP") + #expect(LocaleIdentifier.enMS.rawValue == "en_MS") + #expect(LocaleIdentifier.enMT.rawValue == "en_MT") + #expect(LocaleIdentifier.enMU.rawValue == "en_MU") + #expect(LocaleIdentifier.enMV.rawValue == "en_MV") + #expect(LocaleIdentifier.enMW.rawValue == "en_MW") + #expect(LocaleIdentifier.enMY.rawValue == "en_MY") + #expect(LocaleIdentifier.enNA.rawValue == "en_NA") + #expect(LocaleIdentifier.enNF.rawValue == "en_NF") + #expect(LocaleIdentifier.enNG.rawValue == "en_NG") + #expect(LocaleIdentifier.enNL.rawValue == "en_NL") + #expect(LocaleIdentifier.enNR.rawValue == "en_NR") + #expect(LocaleIdentifier.enNU.rawValue == "en_NU") + #expect(LocaleIdentifier.enNZ.rawValue == "en_NZ") + #expect(LocaleIdentifier.enPG.rawValue == "en_PG") + #expect(LocaleIdentifier.enPH.rawValue == "en_PH") + #expect(LocaleIdentifier.enPK.rawValue == "en_PK") + #expect(LocaleIdentifier.enPN.rawValue == "en_PN") + #expect(LocaleIdentifier.enPR.rawValue == "en_PR") + #expect(LocaleIdentifier.enPW.rawValue == "en_PW") + #expect(LocaleIdentifier.enRW.rawValue == "en_RW") + #expect(LocaleIdentifier.enSB.rawValue == "en_SB") + #expect(LocaleIdentifier.enSC.rawValue == "en_SC") + #expect(LocaleIdentifier.enSD.rawValue == "en_SD") + #expect(LocaleIdentifier.enSE.rawValue == "en_SE") + #expect(LocaleIdentifier.enSG.rawValue == "en_SG") + #expect(LocaleIdentifier.enSH.rawValue == "en_SH") + #expect(LocaleIdentifier.enSI.rawValue == "en_SI") + #expect(LocaleIdentifier.enSL.rawValue == "en_SL") + #expect(LocaleIdentifier.enSS.rawValue == "en_SS") + #expect(LocaleIdentifier.enSX.rawValue == "en_SX") + #expect(LocaleIdentifier.enSZ.rawValue == "en_SZ") + #expect(LocaleIdentifier.enTC.rawValue == "en_TC") + #expect(LocaleIdentifier.enTK.rawValue == "en_TK") + #expect(LocaleIdentifier.enTO.rawValue == "en_TO") + #expect(LocaleIdentifier.enTT.rawValue == "en_TT") + #expect(LocaleIdentifier.enTV.rawValue == "en_TV") + #expect(LocaleIdentifier.enTZ.rawValue == "en_TZ") + #expect(LocaleIdentifier.enUG.rawValue == "en_UG") + #expect(LocaleIdentifier.enUM.rawValue == "en_UM") + #expect(LocaleIdentifier.enUS.rawValue == "en_US") + #expect(LocaleIdentifier.enUSPOSIX.rawValue == "en_US_POSIX") + #expect(LocaleIdentifier.enVC.rawValue == "en_VC") + #expect(LocaleIdentifier.enVG.rawValue == "en_VG") + #expect(LocaleIdentifier.enVI.rawValue == "en_VI") + #expect(LocaleIdentifier.enVU.rawValue == "en_VU") + #expect(LocaleIdentifier.enWS.rawValue == "en_WS") + #expect(LocaleIdentifier.enZA.rawValue == "en_ZA") + #expect(LocaleIdentifier.enZM.rawValue == "en_ZM") + #expect(LocaleIdentifier.enZW.rawValue == "en_ZW") + #expect(LocaleIdentifier.esAR.rawValue == "es_AR") + #expect(LocaleIdentifier.esBO.rawValue == "es_BO") + #expect(LocaleIdentifier.esBR.rawValue == "es_BR") + #expect(LocaleIdentifier.esBZ.rawValue == "es_BZ") + #expect(LocaleIdentifier.esCL.rawValue == "es_CL") + #expect(LocaleIdentifier.esCO.rawValue == "es_CO") + #expect(LocaleIdentifier.esCR.rawValue == "es_CR") + #expect(LocaleIdentifier.esCU.rawValue == "es_CU") + #expect(LocaleIdentifier.esDO.rawValue == "es_DO") + #expect(LocaleIdentifier.esEA.rawValue == "es_EA") + #expect(LocaleIdentifier.esEC.rawValue == "es_EC") + #expect(LocaleIdentifier.esES.rawValue == "es_ES") + #expect(LocaleIdentifier.esGQ.rawValue == "es_GQ") + #expect(LocaleIdentifier.esGT.rawValue == "es_GT") + #expect(LocaleIdentifier.esHN.rawValue == "es_HN") + #expect(LocaleIdentifier.esIC.rawValue == "es_IC") + #expect(LocaleIdentifier.esMX.rawValue == "es_MX") + #expect(LocaleIdentifier.esNI.rawValue == "es_NI") + #expect(LocaleIdentifier.esPA.rawValue == "es_PA") + #expect(LocaleIdentifier.esPE.rawValue == "es_PE") + #expect(LocaleIdentifier.esPH.rawValue == "es_PH") + #expect(LocaleIdentifier.esPR.rawValue == "es_PR") + #expect(LocaleIdentifier.esPY.rawValue == "es_PY") + #expect(LocaleIdentifier.esSV.rawValue == "es_SV") + #expect(LocaleIdentifier.esUS.rawValue == "es_US") + #expect(LocaleIdentifier.esUY.rawValue == "es_UY") + #expect(LocaleIdentifier.esVE.rawValue == "es_VE") + #expect(LocaleIdentifier.etEE.rawValue == "et_EE") + #expect(LocaleIdentifier.euES.rawValue == "eu_ES") + #expect(LocaleIdentifier.faAF.rawValue == "fa_AF") + #expect(LocaleIdentifier.faIR.rawValue == "fa_IR") + #expect(LocaleIdentifier.fiFI.rawValue == "fi_FI") + #expect(LocaleIdentifier.foDK.rawValue == "fo_DK") + #expect(LocaleIdentifier.foFO.rawValue == "fo_FO") + #expect(LocaleIdentifier.frBE.rawValue == "fr_BE") + #expect(LocaleIdentifier.frBF.rawValue == "fr_BF") + #expect(LocaleIdentifier.frBI.rawValue == "fr_BI") + #expect(LocaleIdentifier.frBJ.rawValue == "fr_BJ") + #expect(LocaleIdentifier.frBL.rawValue == "fr_BL") + #expect(LocaleIdentifier.frCA.rawValue == "fr_CA") + #expect(LocaleIdentifier.frCD.rawValue == "fr_CD") + #expect(LocaleIdentifier.frCF.rawValue == "fr_CF") + #expect(LocaleIdentifier.frCG.rawValue == "fr_CG") + #expect(LocaleIdentifier.frCH.rawValue == "fr_CH") + #expect(LocaleIdentifier.frCI.rawValue == "fr_CI") + #expect(LocaleIdentifier.frCM.rawValue == "fr_CM") + #expect(LocaleIdentifier.frDJ.rawValue == "fr_DJ") + #expect(LocaleIdentifier.frDZ.rawValue == "fr_DZ") + #expect(LocaleIdentifier.frFR.rawValue == "fr_FR") + #expect(LocaleIdentifier.frGA.rawValue == "fr_GA") + #expect(LocaleIdentifier.frGF.rawValue == "fr_GF") + #expect(LocaleIdentifier.frGN.rawValue == "fr_GN") + #expect(LocaleIdentifier.frGP.rawValue == "fr_GP") + #expect(LocaleIdentifier.frGQ.rawValue == "fr_GQ") + #expect(LocaleIdentifier.frHT.rawValue == "fr_HT") + #expect(LocaleIdentifier.frKM.rawValue == "fr_KM") + #expect(LocaleIdentifier.frLU.rawValue == "fr_LU") + #expect(LocaleIdentifier.frMA.rawValue == "fr_MA") + #expect(LocaleIdentifier.frMC.rawValue == "fr_MC") + #expect(LocaleIdentifier.frMF.rawValue == "fr_MF") + #expect(LocaleIdentifier.frMG.rawValue == "fr_MG") + #expect(LocaleIdentifier.frML.rawValue == "fr_ML") + #expect(LocaleIdentifier.frMQ.rawValue == "fr_MQ") + #expect(LocaleIdentifier.frMR.rawValue == "fr_MR") + #expect(LocaleIdentifier.frMU.rawValue == "fr_MU") + #expect(LocaleIdentifier.frNC.rawValue == "fr_NC") + #expect(LocaleIdentifier.frNE.rawValue == "fr_NE") + #expect(LocaleIdentifier.frPF.rawValue == "fr_PF") + #expect(LocaleIdentifier.frPM.rawValue == "fr_PM") + #expect(LocaleIdentifier.frRE.rawValue == "fr_RE") + #expect(LocaleIdentifier.frRW.rawValue == "fr_RW") + #expect(LocaleIdentifier.frSC.rawValue == "fr_SC") + #expect(LocaleIdentifier.frSN.rawValue == "fr_SN") + #expect(LocaleIdentifier.frSY.rawValue == "fr_SY") + #expect(LocaleIdentifier.frTD.rawValue == "fr_TD") + #expect(LocaleIdentifier.frTG.rawValue == "fr_TG") + #expect(LocaleIdentifier.frTN.rawValue == "fr_TN") + #expect(LocaleIdentifier.frVU.rawValue == "fr_VU") + #expect(LocaleIdentifier.frWF.rawValue == "fr_WF") + #expect(LocaleIdentifier.frYT.rawValue == "fr_YT") + #expect(LocaleIdentifier.fyNL.rawValue == "fy_NL") + #expect(LocaleIdentifier.gaGB.rawValue == "ga_GB") + #expect(LocaleIdentifier.gaIE.rawValue == "ga_IE") + #expect(LocaleIdentifier.gdGB.rawValue == "gd_GB") + #expect(LocaleIdentifier.glES.rawValue == "gl_ES") + #expect(LocaleIdentifier.gnPY.rawValue == "gn_PY") + #expect(LocaleIdentifier.guIN.rawValue == "gu_IN") + #expect(LocaleIdentifier.gvIM.rawValue == "gv_IM") + #expect(LocaleIdentifier.haGH.rawValue == "ha_GH") + #expect(LocaleIdentifier.haNE.rawValue == "ha_NE") + #expect(LocaleIdentifier.haNG.rawValue == "ha_NG") + #expect(LocaleIdentifier.heIL.rawValue == "he_IL") + #expect(LocaleIdentifier.hiIN.rawValue == "hi_IN") + #expect(LocaleIdentifier.hrBA.rawValue == "hr_BA") + #expect(LocaleIdentifier.hrHR.rawValue == "hr_HR") + #expect(LocaleIdentifier.huHU.rawValue == "hu_HU") + #expect(LocaleIdentifier.hyAM.rawValue == "hy_AM") + #expect(LocaleIdentifier.idID.rawValue == "id_ID") + #expect(LocaleIdentifier.ieEE.rawValue == "ie_EE") + #expect(LocaleIdentifier.igNG.rawValue == "ig_NG") + #expect(LocaleIdentifier.iiCN.rawValue == "ii_CN") + #expect(LocaleIdentifier.isIS.rawValue == "is_IS") + #expect(LocaleIdentifier.itCH.rawValue == "it_CH") + #expect(LocaleIdentifier.itIT.rawValue == "it_IT") + #expect(LocaleIdentifier.itSM.rawValue == "it_SM") + #expect(LocaleIdentifier.itVA.rawValue == "it_VA") + #expect(LocaleIdentifier.iuCA.rawValue == "iu_CA") + #expect(LocaleIdentifier.jaJP.rawValue == "ja_JP") + #expect(LocaleIdentifier.jvID.rawValue == "jv_ID") + #expect(LocaleIdentifier.kaGE.rawValue == "ka_GE") + #expect(LocaleIdentifier.kiKE.rawValue == "ki_KE") + #expect(LocaleIdentifier.kkKZ.rawValue == "kk_KZ") + #expect(LocaleIdentifier.klGL.rawValue == "kl_GL") + #expect(LocaleIdentifier.kmKH.rawValue == "km_KH") + #expect(LocaleIdentifier.knIN.rawValue == "kn_IN") + #expect(LocaleIdentifier.koCN.rawValue == "ko_CN") + #expect(LocaleIdentifier.koKP.rawValue == "ko_KP") + #expect(LocaleIdentifier.koKR.rawValue == "ko_KR") + #expect(LocaleIdentifier.kuTR.rawValue == "ku_TR") + #expect(LocaleIdentifier.kwGB.rawValue == "kw_GB") + #expect(LocaleIdentifier.kyKG.rawValue == "ky_KG") + #expect(LocaleIdentifier.laVA.rawValue == "la_VA") + #expect(LocaleIdentifier.lbLU.rawValue == "lb_LU") + #expect(LocaleIdentifier.lgUG.rawValue == "lg_UG") + #expect(LocaleIdentifier.lnAO.rawValue == "ln_AO") + #expect(LocaleIdentifier.lnCD.rawValue == "ln_CD") + #expect(LocaleIdentifier.lnCF.rawValue == "ln_CF") + #expect(LocaleIdentifier.lnCG.rawValue == "ln_CG") + #expect(LocaleIdentifier.loLA.rawValue == "lo_LA") + #expect(LocaleIdentifier.ltLT.rawValue == "lt_LT") + #expect(LocaleIdentifier.luCD.rawValue == "lu_CD") + #expect(LocaleIdentifier.lvLV.rawValue == "lv_LV") + #expect(LocaleIdentifier.mgMG.rawValue == "mg_MG") + #expect(LocaleIdentifier.miNZ.rawValue == "mi_NZ") + #expect(LocaleIdentifier.mkMK.rawValue == "mk_MK") + #expect(LocaleIdentifier.mlIN.rawValue == "ml_IN") + #expect(LocaleIdentifier.mnMN.rawValue == "mn_MN") + #expect(LocaleIdentifier.mrIN.rawValue == "mr_IN") + #expect(LocaleIdentifier.msBN.rawValue == "ms_BN") + #expect(LocaleIdentifier.msID.rawValue == "ms_ID") + #expect(LocaleIdentifier.msMY.rawValue == "ms_MY") + #expect(LocaleIdentifier.msSG.rawValue == "ms_SG") + #expect(LocaleIdentifier.mtMT.rawValue == "mt_MT") + #expect(LocaleIdentifier.myMM.rawValue == "my_MM") + #expect(LocaleIdentifier.nbNO.rawValue == "nb_NO") + #expect(LocaleIdentifier.nbSJ.rawValue == "nb_SJ") + #expect(LocaleIdentifier.ndZW.rawValue == "nd_ZW") + #expect(LocaleIdentifier.neIN.rawValue == "ne_IN") + #expect(LocaleIdentifier.neNP.rawValue == "ne_NP") + #expect(LocaleIdentifier.nlAW.rawValue == "nl_AW") + #expect(LocaleIdentifier.nlBE.rawValue == "nl_BE") + #expect(LocaleIdentifier.nlBQ.rawValue == "nl_BQ") + #expect(LocaleIdentifier.nlCW.rawValue == "nl_CW") + #expect(LocaleIdentifier.nlNL.rawValue == "nl_NL") + #expect(LocaleIdentifier.nlSR.rawValue == "nl_SR") + #expect(LocaleIdentifier.nlSX.rawValue == "nl_SX") + #expect(LocaleIdentifier.nnNO.rawValue == "nn_NO") + #expect(LocaleIdentifier.nrZA.rawValue == "nr_ZA") + #expect(LocaleIdentifier.nvUS.rawValue == "nv_US") + #expect(LocaleIdentifier.nyMW.rawValue == "ny_MW") + #expect(LocaleIdentifier.ocES.rawValue == "oc_ES") + #expect(LocaleIdentifier.ocFR.rawValue == "oc_FR") + #expect(LocaleIdentifier.omET.rawValue == "om_ET") + #expect(LocaleIdentifier.omKE.rawValue == "om_KE") + #expect(LocaleIdentifier.orIN.rawValue == "or_IN") + #expect(LocaleIdentifier.osGE.rawValue == "os_GE") + #expect(LocaleIdentifier.osRU.rawValue == "os_RU") + #expect(LocaleIdentifier.plPL.rawValue == "pl_PL") + #expect(LocaleIdentifier.psAF.rawValue == "ps_AF") + #expect(LocaleIdentifier.psPK.rawValue == "ps_PK") + #expect(LocaleIdentifier.ptAO.rawValue == "pt_AO") + #expect(LocaleIdentifier.ptBR.rawValue == "pt_BR") + #expect(LocaleIdentifier.ptCH.rawValue == "pt_CH") + #expect(LocaleIdentifier.ptCV.rawValue == "pt_CV") + #expect(LocaleIdentifier.ptGQ.rawValue == "pt_GQ") + #expect(LocaleIdentifier.ptGW.rawValue == "pt_GW") + #expect(LocaleIdentifier.ptLU.rawValue == "pt_LU") + #expect(LocaleIdentifier.ptMO.rawValue == "pt_MO") + #expect(LocaleIdentifier.ptMZ.rawValue == "pt_MZ") + #expect(LocaleIdentifier.ptPT.rawValue == "pt_PT") + #expect(LocaleIdentifier.ptST.rawValue == "pt_ST") + #expect(LocaleIdentifier.ptTL.rawValue == "pt_TL") + #expect(LocaleIdentifier.quBO.rawValue == "qu_BO") + #expect(LocaleIdentifier.quEC.rawValue == "qu_EC") + #expect(LocaleIdentifier.quPE.rawValue == "qu_PE") + #expect(LocaleIdentifier.rmCH.rawValue == "rm_CH") + #expect(LocaleIdentifier.rnBI.rawValue == "rn_BI") + #expect(LocaleIdentifier.roMD.rawValue == "ro_MD") + #expect(LocaleIdentifier.roRO.rawValue == "ro_RO") + #expect(LocaleIdentifier.ruBY.rawValue == "ru_BY") + #expect(LocaleIdentifier.ruKG.rawValue == "ru_KG") + #expect(LocaleIdentifier.ruKZ.rawValue == "ru_KZ") + #expect(LocaleIdentifier.ruMD.rawValue == "ru_MD") + #expect(LocaleIdentifier.ruRU.rawValue == "ru_RU") + #expect(LocaleIdentifier.ruUA.rawValue == "ru_UA") + #expect(LocaleIdentifier.rwRW.rawValue == "rw_RW") + #expect(LocaleIdentifier.saIN.rawValue == "sa_IN") + #expect(LocaleIdentifier.scIT.rawValue == "sc_IT") + #expect(LocaleIdentifier.seFI.rawValue == "se_FI") + #expect(LocaleIdentifier.seNO.rawValue == "se_NO") + #expect(LocaleIdentifier.seSE.rawValue == "se_SE") + #expect(LocaleIdentifier.sgCF.rawValue == "sg_CF") + #expect(LocaleIdentifier.siLK.rawValue == "si_LK") + #expect(LocaleIdentifier.skSK.rawValue == "sk_SK") + #expect(LocaleIdentifier.slSI.rawValue == "sl_SI") + #expect(LocaleIdentifier.snZW.rawValue == "sn_ZW") + #expect(LocaleIdentifier.soDJ.rawValue == "so_DJ") + #expect(LocaleIdentifier.soET.rawValue == "so_ET") + #expect(LocaleIdentifier.soKE.rawValue == "so_KE") + #expect(LocaleIdentifier.soSO.rawValue == "so_SO") + #expect(LocaleIdentifier.sqAL.rawValue == "sq_AL") + #expect(LocaleIdentifier.sqMK.rawValue == "sq_MK") + #expect(LocaleIdentifier.sqXK.rawValue == "sq_XK") + #expect(LocaleIdentifier.ssSZ.rawValue == "ss_SZ") + #expect(LocaleIdentifier.ssZA.rawValue == "ss_ZA") + #expect(LocaleIdentifier.stLS.rawValue == "st_LS") + #expect(LocaleIdentifier.stZA.rawValue == "st_ZA") + #expect(LocaleIdentifier.svAX.rawValue == "sv_AX") + #expect(LocaleIdentifier.svFI.rawValue == "sv_FI") + #expect(LocaleIdentifier.svSE.rawValue == "sv_SE") + #expect(LocaleIdentifier.swCD.rawValue == "sw_CD") + #expect(LocaleIdentifier.swKE.rawValue == "sw_KE") + #expect(LocaleIdentifier.swTZ.rawValue == "sw_TZ") + #expect(LocaleIdentifier.swUG.rawValue == "sw_UG") + #expect(LocaleIdentifier.taIN.rawValue == "ta_IN") + #expect(LocaleIdentifier.taLK.rawValue == "ta_LK") + #expect(LocaleIdentifier.taMY.rawValue == "ta_MY") + #expect(LocaleIdentifier.taSG.rawValue == "ta_SG") + #expect(LocaleIdentifier.teIN.rawValue == "te_IN") + #expect(LocaleIdentifier.tgTJ.rawValue == "tg_TJ") + #expect(LocaleIdentifier.thTH.rawValue == "th_TH") + #expect(LocaleIdentifier.tiER.rawValue == "ti_ER") + #expect(LocaleIdentifier.tiET.rawValue == "ti_ET") + #expect(LocaleIdentifier.tkTM.rawValue == "tk_TM") + #expect(LocaleIdentifier.tnBW.rawValue == "tn_BW") + #expect(LocaleIdentifier.tnZA.rawValue == "tn_ZA") + #expect(LocaleIdentifier.toTO.rawValue == "to_TO") + #expect(LocaleIdentifier.trCY.rawValue == "tr_CY") + #expect(LocaleIdentifier.trTR.rawValue == "tr_TR") + #expect(LocaleIdentifier.tsZA.rawValue == "ts_ZA") + #expect(LocaleIdentifier.ttRU.rawValue == "tt_RU") + #expect(LocaleIdentifier.ugCN.rawValue == "ug_CN") + #expect(LocaleIdentifier.ukUA.rawValue == "uk_UA") + #expect(LocaleIdentifier.urIN.rawValue == "ur_IN") + #expect(LocaleIdentifier.urPK.rawValue == "ur_PK") + #expect(LocaleIdentifier.veZA.rawValue == "ve_ZA") + #expect(LocaleIdentifier.viVN.rawValue == "vi_VN") + #expect(LocaleIdentifier.waBE.rawValue == "wa_BE") + #expect(LocaleIdentifier.woSN.rawValue == "wo_SN") + #expect(LocaleIdentifier.xhZA.rawValue == "xh_ZA") + #expect(LocaleIdentifier.yiUA.rawValue == "yi_UA") + #expect(LocaleIdentifier.yoBJ.rawValue == "yo_BJ") + #expect(LocaleIdentifier.yoNG.rawValue == "yo_NG") + #expect(LocaleIdentifier.zaCN.rawValue == "za_CN") + #expect(LocaleIdentifier.zuZA.rawValue == "zu_ZA") + #expect(LocaleIdentifier.zhCN.rawValue == "zh_CN") + #expect(LocaleIdentifier.zhTW.rawValue == "zh_TW") + #expect(LocaleIdentifier.paIN.rawValue == "pa_IN") + #expect(LocaleIdentifier.azAZ.rawValue == "az_AZ") + #expect(LocaleIdentifier.suID.rawValue == "su_ID") + #expect(LocaleIdentifier.cebPH.rawValue == "ceb_PH") + #expect(LocaleIdentifier.srRS.rawValue == "sr_RS") + } +} diff --git a/Tests/EasyCoreTests/LocaleTests.swift b/Tests/EasyCoreTests/LocaleTests.swift new file mode 100644 index 0000000..0661741 --- /dev/null +++ b/Tests/EasyCoreTests/LocaleTests.swift @@ -0,0 +1,12 @@ +import Testing +import Foundation + +@testable import EasyCore + +@Suite("Locale") +struct LocaleExtension { + @Test + func locales() { + #expect(Locale.locales.count == 440) + } +} diff --git a/Tests/EasyCoreTests/MainThreadTests.swift b/Tests/EasyCoreTests/MainThreadTests.swift new file mode 100644 index 0000000..477bd2e --- /dev/null +++ b/Tests/EasyCoreTests/MainThreadTests.swift @@ -0,0 +1,92 @@ +import Foundation +import Testing + +@testable import EasyCore + +@Suite("MainThread") +struct MainThreadTests { + private actor Flag { + private(set) var value = false + func setTrue() { value = true } + func get() -> Bool { value } + } + + @Test("asyncSafe executes immediately when already on main thread") + func asyncSafeOnMainThread() async throws { + let flag = Flag() + + MainThread.asyncSafe { + Task { await flag.setTrue() } + } + + try await Task.sleep(nanoseconds: 300_000_000) // 0.3s + #expect(await flag.get()) + } + + @Test("asyncSafe executes on main thread when called from background thread") + func asyncSafeFromBackground() async throws { + let flag = Flag() + + try await withCheckedThrowingContinuation { continuation in + DispatchQueue.global().async { + MainThread.asyncSafe { + guard Thread.isMainThread else { return } + Task { await flag.setTrue() } + + // resume needs to run on main thread to avoid thread race + DispatchQueue.main.async { + continuation.resume() + } + } + } + } + + #expect(await flag.get()) + } + + @Test("asyncSafe executes in main thread when on main thread") + func asyncSafeFromMain() async throws { + let flag = Flag() + + try await withCheckedThrowingContinuation { continuation in + DispatchQueue.main.async { + MainThread.asyncSafe { + guard Thread.isMainThread else { return } + Task { await flag.setTrue() } + + // resume needs to run on main thread to avoid thread race + DispatchQueue.main.async { + continuation.resume() + } + } + } + } + + #expect(await flag.get()) + } + + @Test("asyncAfter executes block after specified delay") + func asyncAfterExecutes() async throws { + let flag = Flag() + + MainThread.asyncAfter(seconds: 0.2) { + Task { await flag.setTrue() } + } + + try await Task.sleep(nanoseconds: 300_000_000) // 0.3s + #expect(await flag.get()) + } + + @Test("asyncAfter runs block on main thread") + func asyncAfterRunsOnMainThread() async throws { + let flag = Flag() + + MainThread.asyncAfter(seconds: 0.1) { + guard Thread.isMainThread else { return } + Task { await flag.setTrue() } + } + + try await Task.sleep(nanoseconds: 200_000_000) + #expect(await flag.get()) + } +} diff --git a/Tests/EasyCoreTests/NumberExtensionTests.swift b/Tests/EasyCoreTests/NumberExtensionTests.swift new file mode 100644 index 0000000..7b1e603 --- /dev/null +++ b/Tests/EasyCoreTests/NumberExtensionTests.swift @@ -0,0 +1,183 @@ +import Testing + +@testable import EasyCore + +@Suite("Number") +struct NumberTests { + @Suite("Double validations") + struct DoubleValidationTests { + + @Test("isPositive returns true for values greater than zero") + func positiveValue() { + let value: Double = 3.14 + #expect(value.isPositive) + } + + @Test("isPositive returns false for zero or negative values") + func notPositive() { + #expect((-1.0).isPositive == false) + #expect(0.0.isPositive == false) + } + + @Test("isNegative returns true for values less than zero") + func negativeValue() { + let value: Double = -2.5 + #expect(value.isNegative) + } + + @Test("isNegative returns false for zero or positive values") + func notNegative() { + #expect(1.0.isNegative == false) + #expect(0.0.isNegative == false) + } + + @Test("isZero returns true for exactly 0.0") + func isZeroValue() { + let value: Double = 0.0 + #expect(value.isZero) + } + + @Test("isZero returns false for non-zero values") + func notZero() { + #expect(1.0.isZero == false) + #expect((-0.1).isZero == false) + } + + @Test("isInRange returns true for values within range") + func inRange() { + let value: Double = 5.5 + #expect(value.isInRange(5.0...6.0)) + } + + @Test("isInRange returns false for values outside range") + func outOfRange() { + let value: Double = 7.0 + #expect(value.isInRange(1.0...5.0) == false) + } + + @Test("isInRange returns true for boundary values") + func boundaryValues() { + let value1: Double = 10.0 + let value2: Double = 20.0 + #expect(value1.isInRange(10.0...20.0)) + #expect(value2.isInRange(10.0...20.0)) + } + } + + @Suite("Float validations") + struct FloatValidationTests { + + @Test("isPositive returns true for positive values") + func isPositiveTrue() { + let value: Float = 3.14 + #expect(value.isPositive) + } + + @Test("isPositive returns false for zero and negative values") + func isPositiveFalse() { + #expect(Float(0).isPositive == false) + #expect(Float(-1).isPositive == false) + } + + @Test("isNegative returns true for negative values") + func isNegativeTrue() { + let value: Float = -5.6 + #expect(value.isNegative) + } + + @Test("isNegative returns false for zero and positive values") + func isNegativeFalse() { + #expect(Float(0).isNegative == false) + #expect(Float(2.2).isNegative == false) + } + + @Test("isZero returns true for exactly 0.0") + func isZeroTrue() { + let value: Float = 0.0 + #expect(value.isZero) + } + + @Test("isZero returns false for non-zero values") + func isZeroFalse() { + #expect(Float(1).isZero == false) + #expect(Float(-0.001).isZero == false) + } + + @Test("isInRange returns true for values within range") + func inRangeTrue() { + let value: Float = 7.5 + #expect(value.isInRange(5.0...10.0)) + } + + @Test("isInRange returns false for values outside range") + func inRangeFalse() { + let value: Float = 11.0 + #expect(value.isInRange(0.0...10.0) == false) + } + + @Test("isInRange returns true for values at the boundaries") + func inRangeBoundaries() { + let value1: Float = 0.0 + let value2: Float = 10.0 + #expect(value1.isInRange(0.0...10.0)) + #expect(value2.isInRange(0.0...10.0)) + } + } + + @Suite("Int validations") + struct IntValidationTests { + + @Test("isPositive returns true for values greater than zero") + func isPositiveTrue() { + let value = 10 + #expect(value.isPositive) + } + + @Test("isPositive returns false for zero and negative values") + func isPositiveFalse() { + #expect(0.isPositive == false) + #expect((-5).isPositive == false) + } + + @Test("isNegative returns true for values less than zero") + func isNegativeTrue() { + let value = -42 + #expect(value.isNegative) + } + + @Test("isNegative returns false for zero and positive values") + func isNegativeFalse() { + #expect(0.isNegative == false) + #expect(3.isNegative == false) + } + + @Test("isZero returns true for exactly zero") + func isZeroTrue() { + #expect(0.isZero) + } + + @Test("isZero returns false for non-zero values") + func isZeroFalse() { + #expect(1.isZero == false) + #expect((-1).isZero == false) + } + + @Test("isInRange returns true for values within the range") + func inRangeTrue() { + let value = 50 + #expect(value.isInRange(0...100)) + } + + @Test("isInRange returns false for values outside the range") + func inRangeFalse() { + let value = -10 + #expect(value.isInRange(0...100) == false) + } + + @Test("isInRange returns true for values at the edges of the range") + func inRangeBoundary() { + #expect(0.isInRange(0...10)) + #expect(10.isInRange(0...10)) + } + } +} diff --git a/Tests/EasyCoreTests/OnceTests.swift b/Tests/EasyCoreTests/OnceTests.swift new file mode 100644 index 0000000..50a3d74 --- /dev/null +++ b/Tests/EasyCoreTests/OnceTests.swift @@ -0,0 +1,51 @@ +import Foundation +import Testing + +@testable import EasyCore + +@Suite("Once") +struct OnceTests { + + @Test("Runs the block on first call") + func runsOnce() { + var counter = 0 + let once = Once() + + once.run { + counter += 1 + } + + #expect(counter == 1) + } + + @Test("Does not run the block on subsequent calls") + func doesNotRunAgain() { + var counter = 0 + let once = Once() + + once.run { counter += 1 } + once.run { counter += 1 } + once.run { counter += 1 } + + #expect(counter == 1) + } + + @Test("Thread-safe: only runs block once even when called concurrently") + func threadSafeRunOnce() async throws { + let once = Once() + let counter = Counter() + + await withTaskGroup(of: Void.self) { group in + for _ in 0..<100 { + group.addTask { + once.run { + Task { await counter.increment() } + } + } + } + } + + try await Task.sleep(nanoseconds: 200_000_000) + #expect(await counter.value() == 1) + } +} diff --git a/Tests/EasyCoreTests/OptionalTests.swift b/Tests/EasyCoreTests/OptionalTests.swift new file mode 100644 index 0000000..4f581c9 --- /dev/null +++ b/Tests/EasyCoreTests/OptionalTests.swift @@ -0,0 +1,43 @@ +import Testing + +@testable import EasyCore + +@Suite("Optional") +struct OptionalTests { + @Suite(".or(default:)") + struct OptionalOrDefaultTests { + + @Test("Returns wrapped value when not nil") + func returnsWrappedValue() { + let name: String? = "Letícia" + let result = name.or(default: "Guest") + #expect(result == "Letícia") + } + + @Test("Returns default value when nil") + func returnsDefaultValue() { + let name: String? = nil + let result = name.or(default: "Guest") + #expect(result == "Guest") + } + + @Test("Works with numeric optionals") + func numericOptionals() { + let age: Int? = nil + #expect(age.or(default: 30) == 30) + + let weight: Double? = 58.6 + #expect(weight.or(default: 0.0) == 58.6) + } + + @Test("Works with boolean optionals") + func booleanOptionals() { + let flag: Bool? = nil + #expect(flag.or(default: true) == true) + + let anotherFlag: Bool? = false + #expect(anotherFlag.or(default: true) == false) + } + } + +} diff --git a/Tests/EasyCoreTests/StringTests.swift b/Tests/EasyCoreTests/StringTests.swift new file mode 100644 index 0000000..f7fd332 --- /dev/null +++ b/Tests/EasyCoreTests/StringTests.swift @@ -0,0 +1,111 @@ +import Testing + +@testable import EasyCore + +@Suite("String") +struct StringTests { + @Suite("Optional.isBlank") + struct OptionalStringIsBlankTests { + + @Test("Returns true for nil optional") + func nilOptional() { + let value: String? = nil + #expect(value.isBlank) + } + + @Test("Returns true for empty string") + func emptyString() { + let value: String? = "" + #expect(value.isBlank) + } + + @Test("Returns true for whitespace-only string") + func whitespaceOnly() { + let value: String? = " \n\t " + #expect(value.isBlank) + } + + @Test("Returns false for non-blank string") + func nonBlank() { + let value: String? = "Letícia" + #expect(value.isBlank == false) + } + + @Test("Returns false for string with leading/trailing whitespace but text inside") + func mixedWhitespace() { + let value: String? = " Hello " + #expect(value.isBlank == false) + } + } + + @Suite("String validations") + struct StringValidationTests { + + @Test("removeOcurrencing removes all instances of a substring") + func removeOcurrencingWorks() { + let input = "123.45 BRL" + let result = input.removeOcurrencing("BRL") + #expect(result == "123.45 ") + } + + @Test("isNumeric returns true for strings with only digits") + func isNumericTrue() { + let value = "1234567890" + #expect(value.isNumeric) + } + + @Test("isNumeric returns false for strings with letters or symbols") + func isNumericFalse() { + #expect("123abc".isNumeric == false) + #expect("".isNumeric == false) + #expect(" ".isNumeric == false) + } + + @Test("isAlphabetic returns true for strings with only letters") + func isAlphabeticTrue() { + let value = "HelloWorld" + #expect(value.isAlphabetic) + } + + @Test("isAlphabetic returns false for strings with numbers or symbols") + func isAlphabeticFalse() { + #expect("Hello123".isAlphabetic == false) + #expect(" ".isAlphabetic == false) + #expect("".isAlphabetic == false) + } + + @Test("isBlank returns true for empty and whitespace-only strings") + func isBlankTrue() { + #expect("".isBlank) + #expect(" ".isBlank) + #expect("\n\t ".isBlank) + } + + @Test("isBlank returns false for non-blank strings") + func isBlankFalse() { + #expect("text".isBlank == false) + #expect(" text ".isBlank == false) + } + + @Test("trimmed removes leading and trailing whitespace and newlines") + func trimmedResult() { + let input = " Hello \n" + let result = input.trimmed() + #expect(result == "Hello") + } + + @Test("isEmail returns true for valid email format") + func isEmailTrue() { + #expect("user@example.com".isEmail) + #expect("leticia.speda@domain.co".isEmail) + } + + @Test("isEmail returns false for invalid email format") + func isEmailFalse() { + #expect("not-an-email".isEmail == false) + #expect("missing@domain".isEmail == false) + #expect("missing.com".isEmail == false) + #expect("@missingusername.com".isEmail == false) + } + } +} diff --git a/Tests/EasyCoreTests/ThrottlerTests.swift b/Tests/EasyCoreTests/ThrottlerTests.swift new file mode 100644 index 0000000..0e4340d --- /dev/null +++ b/Tests/EasyCoreTests/ThrottlerTests.swift @@ -0,0 +1,73 @@ +import Foundation +import Testing + +@testable import EasyCore + +@Suite("Throttler") +struct ThrottlerTests { + + @Test("Executes the block on first call") + func executesFirstCall() async throws { + let throttler = Throttler(delay: 0.3) + let tracker = Counter() + + throttler.call { + Task { await tracker.increment() } + } + + try await Task.sleep(nanoseconds: 100_000_000) // wait 0.1s + #expect(await tracker.value() == 1) + } + + @Test("Ignores calls made within the throttle window") + func ignoresRapidCalls() async throws { + let throttler = Throttler(delay: 0.3) + let tracker = Counter() + + throttler.call { + Task { await tracker.increment() } + } + + throttler.call { + Task { await tracker.increment() } + } + + try await Task.sleep(nanoseconds: 400_000_000) // wait 0.4s + #expect(await tracker.value() == 1) + } + + @Test("Allows a second call after the delay has passed") + func allowsCallAfterDelay() async throws { + let throttler = Throttler(delay: 0.2) + let tracker = Counter() + + throttler.call { + Task { await tracker.increment() } + } + + try await Task.sleep(nanoseconds: 300_000_000) // wait 0.3s + + throttler.call { + Task { await tracker.increment() } + } + + try await Task.sleep(nanoseconds: 200_000_000) // wait 0.2s + #expect(await tracker.value() == 2) + } + + @Test("Only first call executes when many calls are made quickly") + func onlyFirstCallDuringBurst() async throws { + let throttler = Throttler(delay: 0.5) + let tracker = Counter() + + for _ in 0..<10 { + throttler.call { + Task { await tracker.increment() } + } + try await Task.sleep(nanoseconds: 50_000_000) // 0.05s + } + + try await Task.sleep(nanoseconds: 600_000_000) // wait after burst + #expect(await tracker.value() == 1) + } +} From 8b24a4aae8cb30a42c9dc676d12759a818e9195b Mon Sep 17 00:00:00 2001 From: Paolo Prodossimo Lopes Date: Mon, 19 May 2025 11:41:33 -0300 Subject: [PATCH 2/4] =?UTF-8?q?=E2=9C=85=20wait=20for=20more=20time?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .swiftpm/xcode/xcshareddata/xcschemes/EasyCore.xcscheme | 4 ++-- Tests/EasyCoreTests/MainThreadTests.swift | 6 ++---- Tests/EasyCoreTests/ThrottlerTests.swift | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/EasyCore.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/EasyCore.xcscheme index d320184..211dd0f 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/EasyCore.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/EasyCore.xcscheme @@ -25,8 +25,8 @@ Date: Mon, 19 May 2025 11:45:55 -0300 Subject: [PATCH 3/4] =?UTF-8?q?=E2=9C=85=20wait=20for=20more=20time?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Tests/EasyCoreTests/ThrottlerTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/EasyCoreTests/ThrottlerTests.swift b/Tests/EasyCoreTests/ThrottlerTests.swift index 1ee9138..2e2e9af 100644 --- a/Tests/EasyCoreTests/ThrottlerTests.swift +++ b/Tests/EasyCoreTests/ThrottlerTests.swift @@ -64,10 +64,10 @@ struct ThrottlerTests { throttler.call { Task { await tracker.increment() } } - try await Task.sleep(nanoseconds: 50_000_000) // 0.05s + try await Task.sleep(nanoseconds: 50000) } - try await Task.sleep(nanoseconds: 6_000_000_000) // wait after burst + try await Task.sleep(nanoseconds: 6000000000) // wait after burst #expect(await tracker.value() == 1) } } From 0c15a5b6686ac71abce0de19eac08f64bd5f1da3 Mon Sep 17 00:00:00 2001 From: Paolo Prodossimo Lopes Date: Tue, 20 May 2025 20:40:13 -0300 Subject: [PATCH 4/4] =?UTF-8?q?=E2=9C=85=20remove=20unescessary=20core=20h?= =?UTF-8?q?ere?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sources/EasyCore/Once.swift.swift | 52 ----------------- Sources/EasyCore/Throttler.swift | 57 ------------------ Tests/EasyCoreTests/OnceTests.swift | 51 ----------------- Tests/EasyCoreTests/ThrottlerTests.swift | 73 ------------------------ 4 files changed, 233 deletions(-) delete mode 100644 Sources/EasyCore/Once.swift.swift delete mode 100644 Sources/EasyCore/Throttler.swift delete mode 100644 Tests/EasyCoreTests/OnceTests.swift delete mode 100644 Tests/EasyCoreTests/ThrottlerTests.swift diff --git a/Sources/EasyCore/Once.swift.swift b/Sources/EasyCore/Once.swift.swift deleted file mode 100644 index 1d71ba7..0000000 --- a/Sources/EasyCore/Once.swift.swift +++ /dev/null @@ -1,52 +0,0 @@ -import Foundation - -/// -/// A utility that ensures a block of code is executed only once, in a thread-safe manner. -/// -/// Use this class when you need to perform an action a single time during the lifecycle of your app, -/// such as initializing a singleton resource, logging once, or performing a setup task. -/// -public final class Once: @unchecked Sendable { - private var hasRun = false - private let lock = NSLock() - - /// - /// Creates a new `Once` instance. - /// - /// ### Example: - /// ```swift - /// let once = Once() - /// ``` - /// - public init() {} - - /// - /// Executes the given block only once. Subsequent calls to this method will do nothing. - /// - /// This method is thread-safe and ensures the block is executed only a single time, - /// even when accessed from multiple threads concurrently. - /// - /// - Parameter block: The closure to execute once. - /// - /// ### Example: - /// ```swift - /// let once = Once() - /// - /// once.run { - /// print("Executed only once") - /// } - /// - /// once.run { - /// print("Will not be executed again") - /// } - /// ``` - /// - public func run(_ block: () -> Void) { - lock.lock() - defer { lock.unlock() } - - guard !hasRun else { return } - hasRun = true - block() - } -} diff --git a/Sources/EasyCore/Throttler.swift b/Sources/EasyCore/Throttler.swift deleted file mode 100644 index 7bedc13..0000000 --- a/Sources/EasyCore/Throttler.swift +++ /dev/null @@ -1,57 +0,0 @@ -import Foundation - -/// -/// A utility that limits the rate at which a block of code can be executed. -/// -/// Use `Throttler` to prevent a block from being called more than once within a specified time interval. -/// This is useful for scenarios like handling rapid user input, scroll events, or network requests where excessive frequency can be problematic. -/// -public final class Throttler { - private let delay: TimeInterval - private var lastExecution: Date? - private var workItem: DispatchWorkItem? - - /// - /// Creates a new `Throttler` instance with the given delay. - /// - /// - Parameter delay: The minimum time interval (in seconds) between allowed executions. - /// - /// ### Example: - /// ```swift - /// let throttler = Throttler(delay: 1.0) - /// ``` - /// - public init(delay: TimeInterval) { - self.delay = delay - } - - /// - /// Attempts to execute the given block if enough time has passed since the last execution. - /// - /// If the time since the last execution is less than the configured delay, the block is ignored. - /// - /// - Parameter block: The closure to execute, if allowed. - /// - /// ### Example: - /// ```swift - /// throttler.call { - /// print("Executed at most once per second") - /// } - /// ``` - /// - public func call(_ block: @escaping () -> Void) { - let now = Date() - - if let last = lastExecution, now.timeIntervalSince(last) < delay { - return - } - - workItem?.cancel() - workItem = DispatchWorkItem(block: block) - lastExecution = now - - if let workItem = workItem { - DispatchQueue.main.async(execute: workItem) - } - } -} diff --git a/Tests/EasyCoreTests/OnceTests.swift b/Tests/EasyCoreTests/OnceTests.swift deleted file mode 100644 index 50a3d74..0000000 --- a/Tests/EasyCoreTests/OnceTests.swift +++ /dev/null @@ -1,51 +0,0 @@ -import Foundation -import Testing - -@testable import EasyCore - -@Suite("Once") -struct OnceTests { - - @Test("Runs the block on first call") - func runsOnce() { - var counter = 0 - let once = Once() - - once.run { - counter += 1 - } - - #expect(counter == 1) - } - - @Test("Does not run the block on subsequent calls") - func doesNotRunAgain() { - var counter = 0 - let once = Once() - - once.run { counter += 1 } - once.run { counter += 1 } - once.run { counter += 1 } - - #expect(counter == 1) - } - - @Test("Thread-safe: only runs block once even when called concurrently") - func threadSafeRunOnce() async throws { - let once = Once() - let counter = Counter() - - await withTaskGroup(of: Void.self) { group in - for _ in 0..<100 { - group.addTask { - once.run { - Task { await counter.increment() } - } - } - } - } - - try await Task.sleep(nanoseconds: 200_000_000) - #expect(await counter.value() == 1) - } -} diff --git a/Tests/EasyCoreTests/ThrottlerTests.swift b/Tests/EasyCoreTests/ThrottlerTests.swift deleted file mode 100644 index 2e2e9af..0000000 --- a/Tests/EasyCoreTests/ThrottlerTests.swift +++ /dev/null @@ -1,73 +0,0 @@ -import Foundation -import Testing - -@testable import EasyCore - -@Suite("Throttler") -struct ThrottlerTests { - - @Test("Executes the block on first call") - func executesFirstCall() async throws { - let throttler = Throttler(delay: 0.3) - let tracker = Counter() - - throttler.call { - Task { await tracker.increment() } - } - - try await Task.sleep(nanoseconds: 100_000_000) // wait 0.1s - #expect(await tracker.value() == 1) - } - - @Test("Ignores calls made within the throttle window") - func ignoresRapidCalls() async throws { - let throttler = Throttler(delay: 0.3) - let tracker = Counter() - - throttler.call { - Task { await tracker.increment() } - } - - throttler.call { - Task { await tracker.increment() } - } - - try await Task.sleep(nanoseconds: 400_000_000) // wait 0.4s - #expect(await tracker.value() == 1) - } - - @Test("Allows a second call after the delay has passed") - func allowsCallAfterDelay() async throws { - let throttler = Throttler(delay: 0.2) - let tracker = Counter() - - throttler.call { - Task { await tracker.increment() } - } - - try await Task.sleep(nanoseconds: 300_000_000) // wait 0.3s - - throttler.call { - Task { await tracker.increment() } - } - - try await Task.sleep(nanoseconds: 200_000_000) // wait 0.2s - #expect(await tracker.value() == 2) - } - - @Test("Only first call executes when many calls are made quickly") - func onlyFirstCallDuringBurst() async throws { - let throttler = Throttler(delay: 0.5) - let tracker = Counter() - - for _ in 0..<10 { - throttler.call { - Task { await tracker.increment() } - } - try await Task.sleep(nanoseconds: 50000) - } - - try await Task.sleep(nanoseconds: 6000000000) // wait after burst - #expect(await tracker.value() == 1) - } -}