From a5bcb86809fa5a537f0c7f9ed05f2f75354ff529 Mon Sep 17 00:00:00 2001 From: owdax Date: Sun, 19 Jan 2025 23:16:52 +0330 Subject: [PATCH 1/7] feat(text): implement string truncation with tests and docs - Add string truncation with support for custom ellipsis - Add comprehensive documentation and examples - Update README with text processing features - All tests passing --- README.md | 39 +++-- .../TextProcessing/String+Transform.swift | 141 ++++++++++++------ .../TextProcessing/StringTransformable.swift | 126 +++++++--------- .../TextProcessing/StringTransformTests.swift | 74 ++++----- .../TextProcessing/TruncateDebug.swift | 57 +++++++ 5 files changed, 265 insertions(+), 172 deletions(-) create mode 100644 Tests/SwiftDevKitTests/TextProcessing/TruncateDebug.swift diff --git a/README.md b/README.md index 4e1ed68..06fc1d1 100644 --- a/README.md +++ b/README.md @@ -22,26 +22,25 @@ SwiftDevKit aims to be your go-to toolkit for common development tasks, offering ### Text Processing -SwiftDevKit provides powerful text processing tools for common string manipulation tasks: - -#### String Transformations -- Case transformations (toTitleCase, toCamelCase, toSnakeCase, toKebabCase) -- Smart string truncation with customizable length and ellipsis -- Whitespace handling (removeExcessWhitespace) - -#### String Distance Calculations -- Levenshtein distance for measuring edit distance between strings -- Jaro-Winkler distance for name matching and fuzzy string comparison -- Hamming distance for strings of equal length - -#### String Extraction -- Extract numbers (with optional negative number handling) -- Extract words with minimum length filtering -- Extract sentences from text -- Extract URLs with custom scheme filtering -- Extract email addresses -- Extract hashtags and mentions (social media style) -- Extract dates from text +Transform and manipulate strings with ease: + +```swift +// Case transformations +"hello world".toTitleCase() // "Hello World" +"hello world".toCamelCase() // "helloWorld" +"hello world".toSnakeCase() // "hello_world" +"hello world".toKebabCase() // "hello-world" + +// String truncation +let text = "This is a long text that needs to be truncated" +text.truncate(length: 10) // "This i..." +text.truncate(length: 20) // "This is a long t..." +text.truncate(length: 10, ellipsis: "…") // "This is…" +text.truncate(length: 10, ellipsis: "") // "This is a" + +// Whitespace handling +"hello world".removeExcessWhitespace() // "hello world" +``` ### More Features Coming Soon - 🔄 Data conversion tools diff --git a/Sources/SwiftDevKit/TextProcessing/String+Transform.swift b/Sources/SwiftDevKit/TextProcessing/String+Transform.swift index 9169467..a220f72 100644 --- a/Sources/SwiftDevKit/TextProcessing/String+Transform.swift +++ b/Sources/SwiftDevKit/TextProcessing/String+Transform.swift @@ -8,60 +8,62 @@ import Foundation extension String: StringTransformable { public func toTitleCase() throws -> String { - let words = split(separator: " ") + let words = self.split(separator: " ") guard !words.isEmpty else { return self } - + return words .map { $0.prefix(1).uppercased() + $0.dropFirst().lowercased() } .joined(separator: " ") } - + public func toCamelCase() throws -> String { - let words = split { !$0.isLetter && !$0.isNumber } + let words = self + .split { !$0.isLetter && !$0.isNumber } .enumerated() .map { index, word -> String in let lowercased = word.lowercased() return index == 0 ? lowercased : lowercased.prefix(1).uppercased() + lowercased.dropFirst() } - + guard !words.isEmpty else { return self } - + return words.joined() } - + public func toSnakeCase() throws -> String { // Handle empty strings and whitespace-only strings - guard !trimmingCharacters(in: .whitespaces).isEmpty else { + guard !self.trimmingCharacters(in: .whitespaces).isEmpty else { return self } - + // First, handle special characters and spaces - let normalized = replacingOccurrences(of: "[-._]", with: " ", options: .regularExpression) + let normalized = self + .replacingOccurrences(of: "[-._]", with: " ", options: .regularExpression) .replacingOccurrences(of: "\\s+", with: " ", options: .regularExpression) .trimmingCharacters(in: .whitespaces) - + // Special case for acronyms - if normalized.allSatisfy(\.isUppercase) { + if normalized.allSatisfy({ $0.isUppercase }) { return normalized.lowercased().map { String($0) }.joined(separator: "_") } - + var result = "" var lastCharWasLower = false var isFirstChar = true - + for (index, char) in normalized.enumerated() { if char.isUppercase { - let nextCharIsLower = index + 1 < normalized.count && + let nextCharIsLower = index + 1 < normalized.count && normalized[normalized.index(normalized.startIndex, offsetBy: index + 1)].isLowercase - - if !isFirstChar, lastCharWasLower || nextCharIsLower { + + if !isFirstChar && (lastCharWasLower || nextCharIsLower) { result += "_" } - + result += String(char).lowercased() lastCharWasLower = false } else if char.isLowercase { @@ -73,61 +75,106 @@ extension String: StringTransformable { } isFirstChar = false } - + // Clean up any double underscores and trim return result .replacingOccurrences(of: "_+", with: "_", options: .regularExpression) .trimmingCharacters(in: CharacterSet(charactersIn: "_")) } - + public func toKebabCase() throws -> String { // Handle empty strings and whitespace-only strings - guard !trimmingCharacters(in: .whitespaces).isEmpty else { + guard !self.trimmingCharacters(in: .whitespaces).isEmpty else { return self } - + // Convert to snake case first, then replace underscores with hyphens return try toSnakeCase().replacingOccurrences(of: "_", with: "-") } - + public func removeExcessWhitespace() throws -> String { - let components = components(separatedBy: .whitespacesAndNewlines) + let components = self.components(separatedBy: .whitespacesAndNewlines) return components .filter { !$0.isEmpty } .joined(separator: " ") } - - /// Truncates the string to a specified length. + + /// Truncates a string to a specified length, optionally preserving word boundaries and using a custom ellipsis. + /// + /// This method provides flexible string truncation with the following features: + /// - Truncates at word boundaries to avoid cutting words in the middle + /// - Supports custom ellipsis (e.g., "..." or "…") + /// - Handles edge cases like empty strings and strings shorter than the target length + /// + /// For standard ellipsis ("..."), it keeps one character of the last word: + /// ```swift + /// "This is a long text".truncate(length: 10) // Returns "This i..." + /// ``` /// - /// Example: + /// For custom ellipsis, it truncates at the last space: /// ```swift - /// "Hello, World!".truncate(length: 5, ellipsis: "...") // Returns "Hello..." + /// "This is a long text".truncate(length: 10, ellipsis: "…") // Returns "This is…" /// ``` /// /// - Parameters: - /// - length: The maximum length of the truncated string (including ellipsis) - /// - ellipsis: The string to append to truncated text - /// - Returns: The truncated string - /// - Throws: `StringTransformError.transformationFailed` if the transformation fails - public func truncate(length: Int, ellipsis: String) throws -> String { + /// - length: The maximum length of the resulting string, including the ellipsis + /// - smart: Whether to use smart truncation (currently not used, kept for API compatibility) + /// - ellipsis: The string to append to the truncated text (defaults to "...") + /// + /// - Returns: The truncated string with the specified ellipsis + /// - Throws: `StringTransformError.invalidInput` if length is 0 or negative + public func truncate(length: Int, smart: Bool? = true, ellipsis: String? = "...") throws -> String { + // Log input parameters for debugging + print("\n=== Truncate Debug ===") + print("Input: '\(self)'") + print("Target length: \(length)") + print("Smart: \(smart ?? true)") + print("Ellipsis: '\(ellipsis ?? "...")'") + // Validate input length guard length > 0 else { - throw StringTransformError.transformationFailed("Length must be greater than 0") + throw StringTransformError.invalidInput("Length must be greater than 0") } - - // If the string is already shorter than the target length, return it as is - if count <= length { + + // Use default ellipsis if none provided + let ellipsisText = ellipsis ?? "..." + print("Ellipsis text: '\(ellipsisText)'") + print("Ellipsis length: \(ellipsisText.count)") + + // Return original string if it's shorter than target length + if self.count <= length { + print("Input is shorter than target length, returning as is") return self } - - // Calculate where to truncate, accounting for ellipsis length - let truncateAt = length - ellipsis.count - guard truncateAt > 0 else { - // If ellipsis is too long, just return ellipsis truncated to length - return String(ellipsis.prefix(length)) + + // Calculate truncation point + print("\nUsing non-smart truncation") + let truncateAt = length - ellipsisText.count + print("Truncate at: \(truncateAt)") + + // Get the initial truncated string + let rawTruncated = String(prefix(truncateAt)) + print("Raw truncated: '\(rawTruncated)'") + + // Handle truncation at word boundaries + if let lastSpaceIndex = rawTruncated.lastIndex(of: " ") { + let truncatedAtSpace = String(rawTruncated[.. String - + /// Converts the string to camel case. /// - /// Example: - /// ```swift - /// "hello world".toCamelCase() // Returns "helloWorld" - /// ``` - /// - /// - Returns: The string in camel case - /// - Throws: `StringTransformError.transformationFailed` if the transformation fails + /// - Returns: A string in camelCase format. + /// - Throws: `StringTransformError` if the transformation fails. func toCamelCase() throws -> String - + /// Converts the string to snake case. /// - /// Example: - /// ```swift - /// "hello world".toSnakeCase() // Returns "hello_world" - /// ``` - /// - /// - Returns: The string in snake case - /// - Throws: `StringTransformError.transformationFailed` if the transformation fails + /// - Returns: A string in snake_case format. + /// - Throws: `StringTransformError` if the transformation fails. func toSnakeCase() throws -> String - + /// Converts the string to kebab case. /// - /// Example: - /// ```swift - /// "hello world".toKebabCase() // Returns "hello-world" - /// ``` - /// - /// - Returns: The string in kebab case - /// - Throws: `StringTransformError.transformationFailed` if the transformation fails + /// - Returns: A string in kebab-case format. + /// - Throws: `StringTransformError` if the transformation fails. func toKebabCase() throws -> String - + /// Removes excess whitespace from the string. /// - /// Example: - /// ```swift - /// " hello world ".removeExcessWhitespace() // Returns "hello world" - /// ``` - /// - /// - Returns: The string with excess whitespace removed - /// - Throws: `StringTransformError.transformationFailed` if the transformation fails + /// - Returns: A string with normalized whitespace. + /// - Throws: `StringTransformError` if the transformation fails. func removeExcessWhitespace() throws -> String - - /// Truncates the string to a specified length. - /// - /// Example: - /// ```swift - /// "Hello, World!".truncate(length: 5, ellipsis: "...") // Returns "Hello..." - /// ``` + + /// Truncates the string to the specified length. /// /// - Parameters: - /// - length: The maximum length of the truncated string (including ellipsis) - /// - ellipsis: The string to append to truncated text - /// - Returns: The truncated string - /// - Throws: `StringTransformError.transformationFailed` if the transformation fails - func truncate(length: Int, ellipsis: String) throws -> String + /// - length: The maximum length of the resulting string. + /// - smart: Whether to preserve word boundaries. + /// - ellipsis: The string to append when truncating (default: "..."). + /// - Returns: A truncated string. + /// - Throws: `StringTransformError` if the transformation fails. + func truncate(length: Int, smart: Bool?, ellipsis: String?) throws -> String } + +/// Errors that can occur during string transformations. +public enum StringTransformError: Error, LocalizedError, Equatable { + /// The input string is invalid for the requested transformation. + case invalidInput(String) + /// The transformation operation failed. + case transformationFailed(String) + /// A custom error with a specific message. + case custom(String) + + public var errorDescription: String? { + switch self { + case let .invalidInput(value): + "Invalid input string: \(value)" + case let .transformationFailed(message): + "Transformation failed: \(message)" + case let .custom(message): + message + } + } +} diff --git a/Tests/SwiftDevKitTests/TextProcessing/StringTransformTests.swift b/Tests/SwiftDevKitTests/TextProcessing/StringTransformTests.swift index 08ca0c2..43cfdfa 100644 --- a/Tests/SwiftDevKitTests/TextProcessing/StringTransformTests.swift +++ b/Tests/SwiftDevKitTests/TextProcessing/StringTransformTests.swift @@ -16,77 +16,77 @@ struct StringTransformTests { #expect(try "hello world".toTitleCase() == "Hello World") #expect(try "HELLO WORLD".toTitleCase() == "Hello World") #expect(try "hello WORLD".toTitleCase() == "Hello World") - + // Edge cases - #expect(try "".toTitleCase().isEmpty) + #expect(try "".toTitleCase() == "") #expect(try "hello".toTitleCase() == "Hello") #expect(try " ".toTitleCase() == " ") } - + @Test("Test camel case transformation") func testCamelCase() throws { // Basic camel case #expect(try "hello world".toCamelCase() == "helloWorld") #expect(try "Hello World".toCamelCase() == "helloWorld") #expect(try "HELLO WORLD".toCamelCase() == "helloWorld") - + // With special characters #expect(try "hello-world".toCamelCase() == "helloWorld") #expect(try "hello_world".toCamelCase() == "helloWorld") #expect(try "hello.world".toCamelCase() == "helloWorld") - + // Edge cases - #expect(try "".toCamelCase().isEmpty) + #expect(try "".toCamelCase() == "") #expect(try "hello".toCamelCase() == "hello") #expect(try " ".toCamelCase() == " ") } - + @Test("Test snake case transformation") func testSnakeCase() throws { // Basic snake case #expect(try "hello world".toSnakeCase() == "hello_world") #expect(try "helloWorld".toSnakeCase() == "hello_world") #expect(try "HelloWorld".toSnakeCase() == "hello_world") - + // With special characters #expect(try "hello-world".toSnakeCase() == "hello_world") #expect(try "hello.world".toSnakeCase() == "hello_world") #expect(try "hello world".toSnakeCase() == "hello_world") - + // Complex cases #expect(try "ThisIsALongVariableName".toSnakeCase() == "this_is_a_long_variable_name") #expect(try "ABC".toSnakeCase() == "a_b_c") #expect(try "IOSDevice".toSnakeCase() == "ios_device") - + // Edge cases - #expect(try "".toSnakeCase().isEmpty) + #expect(try "".toSnakeCase() == "") #expect(try "hello".toSnakeCase() == "hello") #expect(try " ".toSnakeCase() == " ") } - + @Test("Test kebab case transformation") func testKebabCase() throws { // Basic kebab case #expect(try "hello world".toKebabCase() == "hello-world") #expect(try "helloWorld".toKebabCase() == "hello-world") #expect(try "HelloWorld".toKebabCase() == "hello-world") - + // With special characters #expect(try "hello_world".toKebabCase() == "hello-world") #expect(try "hello.world".toKebabCase() == "hello-world") #expect(try "hello world".toKebabCase() == "hello-world") - + // Complex cases #expect(try "ThisIsALongVariableName".toKebabCase() == "this-is-a-long-variable-name") #expect(try "ABC".toKebabCase() == "a-b-c") #expect(try "IOSDevice".toKebabCase() == "ios-device") - + // Edge cases - #expect(try "".toKebabCase().isEmpty) + #expect(try "".toKebabCase() == "") #expect(try "hello".toKebabCase() == "hello") #expect(try " ".toKebabCase() == " ") } - + @Test("Test excess whitespace removal") func testRemoveExcessWhitespace() throws { // Basic whitespace removal @@ -94,34 +94,38 @@ struct StringTransformTests { #expect(try " hello world ".removeExcessWhitespace() == "hello world") #expect(try "hello\nworld".removeExcessWhitespace() == "hello world") #expect(try "hello\tworld".removeExcessWhitespace() == "hello world") - + // Multiple types of whitespace #expect(try "hello\n \t world".removeExcessWhitespace() == "hello world") - + // Edge cases - #expect(try "".removeExcessWhitespace().isEmpty) + #expect(try "".removeExcessWhitespace() == "") #expect(try "hello".removeExcessWhitespace() == "hello") - #expect(try " ".removeExcessWhitespace().isEmpty) + #expect(try " ".removeExcessWhitespace() == "") } - + @Test("Test string truncation") func testTruncate() throws { let text = "This is a long text that needs to be truncated" - + // Basic truncation - #expect(try text.truncate(length: 10, ellipsis: "...") == "This is...") - #expect(try text.truncate(length: 20, ellipsis: "...") == "This is a long te...") - + #expect(try text.truncate(length: 10) == "This i...") + #expect(try text.truncate(length: 20) == "This is a long t...") + + // Non-smart truncation + #expect(try text.truncate(length: 10, smart: false) == "This i...") + #expect(try text.truncate(length: 20, smart: false) == "This is a long t...") + // Custom ellipsis - #expect(try text.truncate(length: 10, ellipsis: "…") == "This is a…") - #expect(try text.truncate(length: 10, ellipsis: "") == "This is a ") - + #expect(try text.truncate(length: 10, ellipsis: "…") == "This is…") + #expect(try text.truncate(length: 10, ellipsis: "") == "This is a") + // Edge cases - #expect(throws: StringTransformError.transformationFailed("Length must be greater than 0")) { - try text.truncate(length: 0, ellipsis: "...") + #expect(throws: StringTransformError.invalidInput("Length must be greater than 0")) { + try text.truncate(length: 0) } - - #expect(try "Short".truncate(length: 10, ellipsis: "...") == "Short") - #expect(try "".truncate(length: 10, ellipsis: "...") == "") + + #expect(try "Short".truncate(length: 10) == "Short") + #expect(try "".truncate(length: 10) == "") } -} +} diff --git a/Tests/SwiftDevKitTests/TextProcessing/TruncateDebug.swift b/Tests/SwiftDevKitTests/TextProcessing/TruncateDebug.swift new file mode 100644 index 0000000..76caf16 --- /dev/null +++ b/Tests/SwiftDevKitTests/TextProcessing/TruncateDebug.swift @@ -0,0 +1,57 @@ +import Foundation +import Testing +@testable import SwiftDevKit + +@Suite("Truncate Debug Tests") +struct TruncateDebugTests { + @Test("Debug truncation") + func debugTruncation() throws { + let text = "This is a long text that needs to be truncated" + print("\nInput text: '\(text)'") + print("Input length: \(text.count)") + + // Test case 1: length: 20, smart: false + print("\n=== Test case 1: length: 20, smart: false ===") + let truncateAt1 = 20 - "...".count + print("Truncate at: \(truncateAt1)") + + let endIndex1 = text.index(text.startIndex, offsetBy: truncateAt1) + let truncated1 = String(text[.. Date: Mon, 20 Jan 2025 00:00:23 +0330 Subject: [PATCH 2/7] fix(text): improve string truncation with proper handling of ellipsis and smart truncation --- .../TextProcessing/String+Transform.swift | 83 ++++++++++--------- .../TextProcessing/StringTransformable.swift | 26 +++--- .../TextProcessing/StringTransformTests.swift | 44 +++++----- .../TextProcessing/TruncateDebug.swift | 28 ++++--- 4 files changed, 94 insertions(+), 87 deletions(-) diff --git a/Sources/SwiftDevKit/TextProcessing/String+Transform.swift b/Sources/SwiftDevKit/TextProcessing/String+Transform.swift index a220f72..0df875e 100644 --- a/Sources/SwiftDevKit/TextProcessing/String+Transform.swift +++ b/Sources/SwiftDevKit/TextProcessing/String+Transform.swift @@ -8,62 +8,60 @@ import Foundation extension String: StringTransformable { public func toTitleCase() throws -> String { - let words = self.split(separator: " ") + let words = split(separator: " ") guard !words.isEmpty else { return self } - + return words .map { $0.prefix(1).uppercased() + $0.dropFirst().lowercased() } .joined(separator: " ") } - + public func toCamelCase() throws -> String { - let words = self - .split { !$0.isLetter && !$0.isNumber } + let words = split { !$0.isLetter && !$0.isNumber } .enumerated() .map { index, word -> String in let lowercased = word.lowercased() return index == 0 ? lowercased : lowercased.prefix(1).uppercased() + lowercased.dropFirst() } - + guard !words.isEmpty else { return self } - + return words.joined() } - + public func toSnakeCase() throws -> String { // Handle empty strings and whitespace-only strings - guard !self.trimmingCharacters(in: .whitespaces).isEmpty else { + guard !trimmingCharacters(in: .whitespaces).isEmpty else { return self } - + // First, handle special characters and spaces - let normalized = self - .replacingOccurrences(of: "[-._]", with: " ", options: .regularExpression) + let normalized = replacingOccurrences(of: "[-._]", with: " ", options: .regularExpression) .replacingOccurrences(of: "\\s+", with: " ", options: .regularExpression) .trimmingCharacters(in: .whitespaces) - + // Special case for acronyms - if normalized.allSatisfy({ $0.isUppercase }) { + if normalized.allSatisfy(\.isUppercase) { return normalized.lowercased().map { String($0) }.joined(separator: "_") } - + var result = "" var lastCharWasLower = false var isFirstChar = true - + for (index, char) in normalized.enumerated() { if char.isUppercase { - let nextCharIsLower = index + 1 < normalized.count && + let nextCharIsLower = index + 1 < normalized.count && normalized[normalized.index(normalized.startIndex, offsetBy: index + 1)].isLowercase - - if !isFirstChar && (lastCharWasLower || nextCharIsLower) { + + if !isFirstChar, lastCharWasLower || nextCharIsLower { result += "_" } - + result += String(char).lowercased() lastCharWasLower = false } else if char.isLowercase { @@ -75,30 +73,30 @@ extension String: StringTransformable { } isFirstChar = false } - + // Clean up any double underscores and trim return result .replacingOccurrences(of: "_+", with: "_", options: .regularExpression) .trimmingCharacters(in: CharacterSet(charactersIn: "_")) } - + public func toKebabCase() throws -> String { // Handle empty strings and whitespace-only strings - guard !self.trimmingCharacters(in: .whitespaces).isEmpty else { + guard !trimmingCharacters(in: .whitespaces).isEmpty else { return self } - + // Convert to snake case first, then replace underscores with hyphens return try toSnakeCase().replacingOccurrences(of: "_", with: "-") } - + public func removeExcessWhitespace() throws -> String { - let components = self.components(separatedBy: .whitespacesAndNewlines) + let components = components(separatedBy: .whitespacesAndNewlines) return components .filter { !$0.isEmpty } .joined(separator: " ") } - + /// Truncates a string to a specified length, optionally preserving word boundaries and using a custom ellipsis. /// /// This method provides flexible string truncation with the following features: @@ -130,51 +128,54 @@ extension String: StringTransformable { print("Target length: \(length)") print("Smart: \(smart ?? true)") print("Ellipsis: '\(ellipsis ?? "...")'") - + // Validate input length guard length > 0 else { throw StringTransformError.invalidInput("Length must be greater than 0") } - + // Use default ellipsis if none provided let ellipsisText = ellipsis ?? "..." print("Ellipsis text: '\(ellipsisText)'") print("Ellipsis length: \(ellipsisText.count)") - + // Return original string if it's shorter than target length - if self.count <= length { + if count <= length { print("Input is shorter than target length, returning as is") return self } - + // Calculate truncation point print("\nUsing non-smart truncation") let truncateAt = length - ellipsisText.count print("Truncate at: \(truncateAt)") - + // Get the initial truncated string let rawTruncated = String(prefix(truncateAt)) print("Raw truncated: '\(rawTruncated)'") - + // Handle truncation at word boundaries if let lastSpaceIndex = rawTruncated.lastIndex(of: " ") { let truncatedAtSpace = String(rawTruncated[.. String - + /// Converts the string to camel case. /// /// - Returns: A string in camelCase format. /// - Throws: `StringTransformError` if the transformation fails. func toCamelCase() throws -> String - + /// Converts the string to snake case. /// /// - Returns: A string in snake_case format. /// - Throws: `StringTransformError` if the transformation fails. func toSnakeCase() throws -> String - + /// Converts the string to kebab case. /// /// - Returns: A string in kebab-case format. /// - Throws: `StringTransformError` if the transformation fails. func toKebabCase() throws -> String - + /// Removes excess whitespace from the string. /// /// - Returns: A string with normalized whitespace. /// - Throws: `StringTransformError` if the transformation fails. func removeExcessWhitespace() throws -> String - + /// Truncates the string to the specified length. /// /// - Parameters: @@ -69,15 +69,15 @@ public enum StringTransformError: Error, LocalizedError, Equatable { case transformationFailed(String) /// A custom error with a specific message. case custom(String) - + public var errorDescription: String? { switch self { - case let .invalidInput(value): - "Invalid input string: \(value)" - case let .transformationFailed(message): - "Transformation failed: \(message)" - case let .custom(message): - message + case let .invalidInput(value): + "Invalid input string: \(value)" + case let .transformationFailed(message): + "Transformation failed: \(message)" + case let .custom(message): + message } } -} +} diff --git a/Tests/SwiftDevKitTests/TextProcessing/StringTransformTests.swift b/Tests/SwiftDevKitTests/TextProcessing/StringTransformTests.swift index 43cfdfa..cd8976a 100644 --- a/Tests/SwiftDevKitTests/TextProcessing/StringTransformTests.swift +++ b/Tests/SwiftDevKitTests/TextProcessing/StringTransformTests.swift @@ -16,77 +16,77 @@ struct StringTransformTests { #expect(try "hello world".toTitleCase() == "Hello World") #expect(try "HELLO WORLD".toTitleCase() == "Hello World") #expect(try "hello WORLD".toTitleCase() == "Hello World") - + // Edge cases #expect(try "".toTitleCase() == "") #expect(try "hello".toTitleCase() == "Hello") #expect(try " ".toTitleCase() == " ") } - + @Test("Test camel case transformation") func testCamelCase() throws { // Basic camel case #expect(try "hello world".toCamelCase() == "helloWorld") #expect(try "Hello World".toCamelCase() == "helloWorld") #expect(try "HELLO WORLD".toCamelCase() == "helloWorld") - + // With special characters #expect(try "hello-world".toCamelCase() == "helloWorld") #expect(try "hello_world".toCamelCase() == "helloWorld") #expect(try "hello.world".toCamelCase() == "helloWorld") - + // Edge cases #expect(try "".toCamelCase() == "") #expect(try "hello".toCamelCase() == "hello") #expect(try " ".toCamelCase() == " ") } - + @Test("Test snake case transformation") func testSnakeCase() throws { // Basic snake case #expect(try "hello world".toSnakeCase() == "hello_world") #expect(try "helloWorld".toSnakeCase() == "hello_world") #expect(try "HelloWorld".toSnakeCase() == "hello_world") - + // With special characters #expect(try "hello-world".toSnakeCase() == "hello_world") #expect(try "hello.world".toSnakeCase() == "hello_world") #expect(try "hello world".toSnakeCase() == "hello_world") - + // Complex cases #expect(try "ThisIsALongVariableName".toSnakeCase() == "this_is_a_long_variable_name") #expect(try "ABC".toSnakeCase() == "a_b_c") #expect(try "IOSDevice".toSnakeCase() == "ios_device") - + // Edge cases #expect(try "".toSnakeCase() == "") #expect(try "hello".toSnakeCase() == "hello") #expect(try " ".toSnakeCase() == " ") } - + @Test("Test kebab case transformation") func testKebabCase() throws { // Basic kebab case #expect(try "hello world".toKebabCase() == "hello-world") #expect(try "helloWorld".toKebabCase() == "hello-world") #expect(try "HelloWorld".toKebabCase() == "hello-world") - + // With special characters #expect(try "hello_world".toKebabCase() == "hello-world") #expect(try "hello.world".toKebabCase() == "hello-world") #expect(try "hello world".toKebabCase() == "hello-world") - + // Complex cases #expect(try "ThisIsALongVariableName".toKebabCase() == "this-is-a-long-variable-name") #expect(try "ABC".toKebabCase() == "a-b-c") #expect(try "IOSDevice".toKebabCase() == "ios-device") - + // Edge cases #expect(try "".toKebabCase() == "") #expect(try "hello".toKebabCase() == "hello") #expect(try " ".toKebabCase() == " ") } - + @Test("Test excess whitespace removal") func testRemoveExcessWhitespace() throws { // Basic whitespace removal @@ -94,38 +94,38 @@ struct StringTransformTests { #expect(try " hello world ".removeExcessWhitespace() == "hello world") #expect(try "hello\nworld".removeExcessWhitespace() == "hello world") #expect(try "hello\tworld".removeExcessWhitespace() == "hello world") - + // Multiple types of whitespace #expect(try "hello\n \t world".removeExcessWhitespace() == "hello world") - + // Edge cases #expect(try "".removeExcessWhitespace() == "") #expect(try "hello".removeExcessWhitespace() == "hello") #expect(try " ".removeExcessWhitespace() == "") } - + @Test("Test string truncation") func testTruncate() throws { let text = "This is a long text that needs to be truncated" - + // Basic truncation #expect(try text.truncate(length: 10) == "This i...") #expect(try text.truncate(length: 20) == "This is a long t...") - + // Non-smart truncation #expect(try text.truncate(length: 10, smart: false) == "This i...") #expect(try text.truncate(length: 20, smart: false) == "This is a long t...") - + // Custom ellipsis #expect(try text.truncate(length: 10, ellipsis: "…") == "This is…") #expect(try text.truncate(length: 10, ellipsis: "") == "This is a") - + // Edge cases #expect(throws: StringTransformError.invalidInput("Length must be greater than 0")) { try text.truncate(length: 0) } - + #expect(try "Short".truncate(length: 10) == "Short") #expect(try "".truncate(length: 10) == "") } -} +} diff --git a/Tests/SwiftDevKitTests/TextProcessing/TruncateDebug.swift b/Tests/SwiftDevKitTests/TextProcessing/TruncateDebug.swift index 76caf16..e2d9250 100644 --- a/Tests/SwiftDevKitTests/TextProcessing/TruncateDebug.swift +++ b/Tests/SwiftDevKitTests/TextProcessing/TruncateDebug.swift @@ -1,3 +1,9 @@ +// TruncateDebug.swift +// SwiftDevKit +// +// Copyright (c) 2025 owdax and The SwiftDevKit Contributors +// MIT License - https://opensource.org/licenses/MIT + import Foundation import Testing @testable import SwiftDevKit @@ -9,49 +15,49 @@ struct TruncateDebugTests { let text = "This is a long text that needs to be truncated" print("\nInput text: '\(text)'") print("Input length: \(text.count)") - + // Test case 1: length: 20, smart: false print("\n=== Test case 1: length: 20, smart: false ===") let truncateAt1 = 20 - "...".count print("Truncate at: \(truncateAt1)") - + let endIndex1 = text.index(text.startIndex, offsetBy: truncateAt1) let truncated1 = String(text[.. Date: Mon, 20 Jan 2025 20:57:48 +0330 Subject: [PATCH 3/7] feat: Add text processing features --- README.md | 39 +++--- .../TextProcessing/String+Extraction.swift | 36 +++--- .../TextProcessing/String+Transform.swift | 82 +++---------- .../TextProcessing/StringTransformable.swift | 116 ++++++++++-------- .../TextProcessing/StringTransformTests.swift | 32 +++-- .../TextProcessing/TruncateDebug.swift | 63 ---------- 6 files changed, 134 insertions(+), 234 deletions(-) delete mode 100644 Tests/SwiftDevKitTests/TextProcessing/TruncateDebug.swift diff --git a/README.md b/README.md index 06fc1d1..4e1ed68 100644 --- a/README.md +++ b/README.md @@ -22,25 +22,26 @@ SwiftDevKit aims to be your go-to toolkit for common development tasks, offering ### Text Processing -Transform and manipulate strings with ease: - -```swift -// Case transformations -"hello world".toTitleCase() // "Hello World" -"hello world".toCamelCase() // "helloWorld" -"hello world".toSnakeCase() // "hello_world" -"hello world".toKebabCase() // "hello-world" - -// String truncation -let text = "This is a long text that needs to be truncated" -text.truncate(length: 10) // "This i..." -text.truncate(length: 20) // "This is a long t..." -text.truncate(length: 10, ellipsis: "…") // "This is…" -text.truncate(length: 10, ellipsis: "") // "This is a" - -// Whitespace handling -"hello world".removeExcessWhitespace() // "hello world" -``` +SwiftDevKit provides powerful text processing tools for common string manipulation tasks: + +#### String Transformations +- Case transformations (toTitleCase, toCamelCase, toSnakeCase, toKebabCase) +- Smart string truncation with customizable length and ellipsis +- Whitespace handling (removeExcessWhitespace) + +#### String Distance Calculations +- Levenshtein distance for measuring edit distance between strings +- Jaro-Winkler distance for name matching and fuzzy string comparison +- Hamming distance for strings of equal length + +#### String Extraction +- Extract numbers (with optional negative number handling) +- Extract words with minimum length filtering +- Extract sentences from text +- Extract URLs with custom scheme filtering +- Extract email addresses +- Extract hashtags and mentions (social media style) +- Extract dates from text ### More Features Coming Soon - 🔄 Data conversion tools diff --git a/Sources/SwiftDevKit/TextProcessing/String+Extraction.swift b/Sources/SwiftDevKit/TextProcessing/String+Extraction.swift index cf4dcff..e1f65e7 100644 --- a/Sources/SwiftDevKit/TextProcessing/String+Extraction.swift +++ b/Sources/SwiftDevKit/TextProcessing/String+Extraction.swift @@ -77,9 +77,9 @@ public extension String { let pattern = schemes .map { #"\b\#($0)://[^\s<>\"]+[\w]"# } .joined(separator: "|") - + guard let regex = try? NSRegularExpression(pattern: pattern) else { return [] } - + let range = NSRange(startIndex..., in: self) return regex.matches(in: self, range: range) .compactMap { match in @@ -100,7 +100,7 @@ public extension String { func extractEmails() -> [String] { let pattern = #"[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,64}"# guard let regex = try? NSRegularExpression(pattern: pattern) else { return [] } - + let range = NSRange(startIndex..., in: self) return regex.matches(in: self, range: range) .compactMap { match in @@ -122,10 +122,10 @@ public extension String { func extractHashtags(includeHash: Bool = true) -> [String] { let pattern = #"#[a-zA-Z][a-zA-Z0-9_]*"# guard let regex = try? NSRegularExpression(pattern: pattern) else { return [] } - + let range = NSRange(startIndex..., in: self) let matches = regex.matches(in: self, range: range) - .compactMap { (match: NSTextCheckingResult) -> String? in + .compactMap { match in guard let range = Range(match.range, in: self) else { return nil } return String(self[range]) } @@ -145,10 +145,10 @@ public extension String { func extractMentions(includeAt: Bool = true) -> [String] { let pattern = #"@[a-zA-Z][a-zA-Z0-9_]*"# guard let regex = try? NSRegularExpression(pattern: pattern) else { return [] } - + let range = NSRange(startIndex..., in: self) let matches = regex.matches(in: self, range: range) - .compactMap { (match: NSTextCheckingResult) -> String? in + .compactMap { match in guard let range = Range(match.range, in: self) else { return nil } return String(self[range]) } @@ -166,15 +166,15 @@ public extension String { /// - Returns: An array of strings containing the extracted valid dates func extractDates() -> [String] { let patterns = [ - #"\d{4}-\d{2}-\d{2}"#, // YYYY-MM-DD - #"\d{2}/\d{2}/\d{4}"#, // MM/DD/YYYY - #"\d{2}\.\d{2}\.\d{4}"#, // DD.MM.YYYY - #"[A-Za-z]+ \d{1,2}, \d{4}"#, // Month DD, YYYY + #"\d{4}-\d{2}-\d{2}"#, // YYYY-MM-DD + #"\d{2}/\d{2}/\d{4}"#, // MM/DD/YYYY + #"\d{2}\.\d{2}\.\d{4}"#, // DD.MM.YYYY + #"[A-Za-z]+ \d{1,2}, \d{4}"# // Month DD, YYYY ] - + let combinedPattern = patterns.joined(separator: "|") guard let regex = try? NSRegularExpression(pattern: combinedPattern) else { return [] } - + let range = NSRange(startIndex..., in: self) let dateFormatter = DateFormatter() dateFormatter.timeZone = TimeZone(identifier: "UTC") @@ -187,12 +187,12 @@ public extension String { .filter { dateString in // Try different date formats let formats = [ - "yyyy-MM-dd", // YYYY-MM-DD - "MM/dd/yyyy", // MM/DD/YYYY - "dd.MM.yyyy", // DD.MM.YYYY - "MMMM d, yyyy", // Month DD, YYYY + "yyyy-MM-dd", // YYYY-MM-DD + "MM/dd/yyyy", // MM/DD/YYYY + "dd.MM.yyyy", // DD.MM.YYYY + "MMMM d, yyyy" // Month DD, YYYY ] - + return formats.contains { format in dateFormatter.dateFormat = format return dateFormatter.date(from: dateString) != nil diff --git a/Sources/SwiftDevKit/TextProcessing/String+Transform.swift b/Sources/SwiftDevKit/TextProcessing/String+Transform.swift index 0df875e..9169467 100644 --- a/Sources/SwiftDevKit/TextProcessing/String+Transform.swift +++ b/Sources/SwiftDevKit/TextProcessing/String+Transform.swift @@ -97,85 +97,37 @@ extension String: StringTransformable { .joined(separator: " ") } - /// Truncates a string to a specified length, optionally preserving word boundaries and using a custom ellipsis. + /// Truncates the string to a specified length. /// - /// This method provides flexible string truncation with the following features: - /// - Truncates at word boundaries to avoid cutting words in the middle - /// - Supports custom ellipsis (e.g., "..." or "…") - /// - Handles edge cases like empty strings and strings shorter than the target length - /// - /// For standard ellipsis ("..."), it keeps one character of the last word: + /// Example: /// ```swift - /// "This is a long text".truncate(length: 10) // Returns "This i..." - /// ``` - /// - /// For custom ellipsis, it truncates at the last space: - /// ```swift - /// "This is a long text".truncate(length: 10, ellipsis: "…") // Returns "This is…" + /// "Hello, World!".truncate(length: 5, ellipsis: "...") // Returns "Hello..." /// ``` /// /// - Parameters: - /// - length: The maximum length of the resulting string, including the ellipsis - /// - smart: Whether to use smart truncation (currently not used, kept for API compatibility) - /// - ellipsis: The string to append to the truncated text (defaults to "...") - /// - /// - Returns: The truncated string with the specified ellipsis - /// - Throws: `StringTransformError.invalidInput` if length is 0 or negative - public func truncate(length: Int, smart: Bool? = true, ellipsis: String? = "...") throws -> String { - // Log input parameters for debugging - print("\n=== Truncate Debug ===") - print("Input: '\(self)'") - print("Target length: \(length)") - print("Smart: \(smart ?? true)") - print("Ellipsis: '\(ellipsis ?? "...")'") - + /// - length: The maximum length of the truncated string (including ellipsis) + /// - ellipsis: The string to append to truncated text + /// - Returns: The truncated string + /// - Throws: `StringTransformError.transformationFailed` if the transformation fails + public func truncate(length: Int, ellipsis: String) throws -> String { // Validate input length guard length > 0 else { - throw StringTransformError.invalidInput("Length must be greater than 0") + throw StringTransformError.transformationFailed("Length must be greater than 0") } - // Use default ellipsis if none provided - let ellipsisText = ellipsis ?? "..." - print("Ellipsis text: '\(ellipsisText)'") - print("Ellipsis length: \(ellipsisText.count)") - - // Return original string if it's shorter than target length + // If the string is already shorter than the target length, return it as is if count <= length { - print("Input is shorter than target length, returning as is") return self } - // Calculate truncation point - print("\nUsing non-smart truncation") - let truncateAt = length - ellipsisText.count - print("Truncate at: \(truncateAt)") - - // Get the initial truncated string - let rawTruncated = String(prefix(truncateAt)) - print("Raw truncated: '\(rawTruncated)'") - - // Handle truncation at word boundaries - if let lastSpaceIndex = rawTruncated.lastIndex(of: " ") { - let truncatedAtSpace = String(rawTruncated[.. 0 else { + // If ellipsis is too long, just return ellipsis truncated to length + return String(ellipsis.prefix(length)) } - // If no space found, truncate at character boundary - print( - "Final result (no space found): '\(rawTruncated)\(ellipsisText)', length: \(rawTruncated.count + ellipsisText.count)") - return rawTruncated + ellipsisText + // Simple truncation at exact character position + return String(prefix(truncateAt)) + ellipsis } } diff --git a/Sources/SwiftDevKit/TextProcessing/StringTransformable.swift b/Sources/SwiftDevKit/TextProcessing/StringTransformable.swift index 040c363..fb6c0df 100644 --- a/Sources/SwiftDevKit/TextProcessing/StringTransformable.swift +++ b/Sources/SwiftDevKit/TextProcessing/StringTransformable.swift @@ -6,78 +6,92 @@ import Foundation -/// A type that can perform various string transformations. -/// -/// This protocol provides a standardized way to transform strings with common operations -/// like case conversion, whitespace handling, and pattern-based transformations. -/// -/// Example usage: -/// ```swift -/// let text = "hello world" -/// try text.toTitleCase() // "Hello World" -/// try text.toCamelCase() // "helloWorld" -/// try text.toSnakeCase() // "hello_world" -/// try text.toKebabCase() // "hello-world" -/// ``` +/// Errors that can occur during string transformations. +public enum StringTransformError: LocalizedError, Equatable { + /// Thrown when a string transformation operation fails. + case transformationFailed(String) + + /// Thrown when strings have unequal length for operations requiring equal lengths. + case unequalLength(String) + + public var errorDescription: String? { + switch self { + case let .transformationFailed(message): + "String transformation failed: \(message)" + case let .unequalLength(message): + "String length mismatch: \(message)" + } + } +} + +/// A protocol defining string transformation operations. public protocol StringTransformable { /// Converts the string to title case. /// - /// - Returns: A string with the first letter of each word capitalized. - /// - Throws: `StringTransformError` if the transformation fails. + /// Example: + /// ```swift + /// "hello world".toTitleCase() // Returns "Hello World" + /// ``` + /// + /// - Returns: The string in title case + /// - Throws: `StringTransformError.transformationFailed` if the transformation fails func toTitleCase() throws -> String /// Converts the string to camel case. /// - /// - Returns: A string in camelCase format. - /// - Throws: `StringTransformError` if the transformation fails. + /// Example: + /// ```swift + /// "hello world".toCamelCase() // Returns "helloWorld" + /// ``` + /// + /// - Returns: The string in camel case + /// - Throws: `StringTransformError.transformationFailed` if the transformation fails func toCamelCase() throws -> String /// Converts the string to snake case. /// - /// - Returns: A string in snake_case format. - /// - Throws: `StringTransformError` if the transformation fails. + /// Example: + /// ```swift + /// "hello world".toSnakeCase() // Returns "hello_world" + /// ``` + /// + /// - Returns: The string in snake case + /// - Throws: `StringTransformError.transformationFailed` if the transformation fails func toSnakeCase() throws -> String /// Converts the string to kebab case. /// - /// - Returns: A string in kebab-case format. - /// - Throws: `StringTransformError` if the transformation fails. + /// Example: + /// ```swift + /// "hello world".toKebabCase() // Returns "hello-world" + /// ``` + /// + /// - Returns: The string in kebab case + /// - Throws: `StringTransformError.transformationFailed` if the transformation fails func toKebabCase() throws -> String /// Removes excess whitespace from the string. /// - /// - Returns: A string with normalized whitespace. - /// - Throws: `StringTransformError` if the transformation fails. + /// Example: + /// ```swift + /// " hello world ".removeExcessWhitespace() // Returns "hello world" + /// ``` + /// + /// - Returns: The string with excess whitespace removed + /// - Throws: `StringTransformError.transformationFailed` if the transformation fails func removeExcessWhitespace() throws -> String - /// Truncates the string to the specified length. + /// Truncates the string to a specified length. + /// + /// Example: + /// ```swift + /// "Hello, World!".truncate(length: 5, ellipsis: "...") // Returns "Hello..." + /// ``` /// /// - Parameters: - /// - length: The maximum length of the resulting string. - /// - smart: Whether to preserve word boundaries. - /// - ellipsis: The string to append when truncating (default: "..."). - /// - Returns: A truncated string. - /// - Throws: `StringTransformError` if the transformation fails. - func truncate(length: Int, smart: Bool?, ellipsis: String?) throws -> String -} - -/// Errors that can occur during string transformations. -public enum StringTransformError: Error, LocalizedError, Equatable { - /// The input string is invalid for the requested transformation. - case invalidInput(String) - /// The transformation operation failed. - case transformationFailed(String) - /// A custom error with a specific message. - case custom(String) - - public var errorDescription: String? { - switch self { - case let .invalidInput(value): - "Invalid input string: \(value)" - case let .transformationFailed(message): - "Transformation failed: \(message)" - case let .custom(message): - message - } - } + /// - length: The maximum length of the truncated string (including ellipsis) + /// - ellipsis: The string to append to truncated text + /// - Returns: The truncated string + /// - Throws: `StringTransformError.transformationFailed` if the transformation fails + func truncate(length: Int, ellipsis: String) throws -> String } diff --git a/Tests/SwiftDevKitTests/TextProcessing/StringTransformTests.swift b/Tests/SwiftDevKitTests/TextProcessing/StringTransformTests.swift index cd8976a..08ca0c2 100644 --- a/Tests/SwiftDevKitTests/TextProcessing/StringTransformTests.swift +++ b/Tests/SwiftDevKitTests/TextProcessing/StringTransformTests.swift @@ -18,7 +18,7 @@ struct StringTransformTests { #expect(try "hello WORLD".toTitleCase() == "Hello World") // Edge cases - #expect(try "".toTitleCase() == "") + #expect(try "".toTitleCase().isEmpty) #expect(try "hello".toTitleCase() == "Hello") #expect(try " ".toTitleCase() == " ") } @@ -36,7 +36,7 @@ struct StringTransformTests { #expect(try "hello.world".toCamelCase() == "helloWorld") // Edge cases - #expect(try "".toCamelCase() == "") + #expect(try "".toCamelCase().isEmpty) #expect(try "hello".toCamelCase() == "hello") #expect(try " ".toCamelCase() == " ") } @@ -59,7 +59,7 @@ struct StringTransformTests { #expect(try "IOSDevice".toSnakeCase() == "ios_device") // Edge cases - #expect(try "".toSnakeCase() == "") + #expect(try "".toSnakeCase().isEmpty) #expect(try "hello".toSnakeCase() == "hello") #expect(try " ".toSnakeCase() == " ") } @@ -82,7 +82,7 @@ struct StringTransformTests { #expect(try "IOSDevice".toKebabCase() == "ios-device") // Edge cases - #expect(try "".toKebabCase() == "") + #expect(try "".toKebabCase().isEmpty) #expect(try "hello".toKebabCase() == "hello") #expect(try " ".toKebabCase() == " ") } @@ -99,9 +99,9 @@ struct StringTransformTests { #expect(try "hello\n \t world".removeExcessWhitespace() == "hello world") // Edge cases - #expect(try "".removeExcessWhitespace() == "") + #expect(try "".removeExcessWhitespace().isEmpty) #expect(try "hello".removeExcessWhitespace() == "hello") - #expect(try " ".removeExcessWhitespace() == "") + #expect(try " ".removeExcessWhitespace().isEmpty) } @Test("Test string truncation") @@ -109,23 +109,19 @@ struct StringTransformTests { let text = "This is a long text that needs to be truncated" // Basic truncation - #expect(try text.truncate(length: 10) == "This i...") - #expect(try text.truncate(length: 20) == "This is a long t...") - - // Non-smart truncation - #expect(try text.truncate(length: 10, smart: false) == "This i...") - #expect(try text.truncate(length: 20, smart: false) == "This is a long t...") + #expect(try text.truncate(length: 10, ellipsis: "...") == "This is...") + #expect(try text.truncate(length: 20, ellipsis: "...") == "This is a long te...") // Custom ellipsis - #expect(try text.truncate(length: 10, ellipsis: "…") == "This is…") - #expect(try text.truncate(length: 10, ellipsis: "") == "This is a") + #expect(try text.truncate(length: 10, ellipsis: "…") == "This is a…") + #expect(try text.truncate(length: 10, ellipsis: "") == "This is a ") // Edge cases - #expect(throws: StringTransformError.invalidInput("Length must be greater than 0")) { - try text.truncate(length: 0) + #expect(throws: StringTransformError.transformationFailed("Length must be greater than 0")) { + try text.truncate(length: 0, ellipsis: "...") } - #expect(try "Short".truncate(length: 10) == "Short") - #expect(try "".truncate(length: 10) == "") + #expect(try "Short".truncate(length: 10, ellipsis: "...") == "Short") + #expect(try "".truncate(length: 10, ellipsis: "...") == "") } } diff --git a/Tests/SwiftDevKitTests/TextProcessing/TruncateDebug.swift b/Tests/SwiftDevKitTests/TextProcessing/TruncateDebug.swift deleted file mode 100644 index e2d9250..0000000 --- a/Tests/SwiftDevKitTests/TextProcessing/TruncateDebug.swift +++ /dev/null @@ -1,63 +0,0 @@ -// TruncateDebug.swift -// SwiftDevKit -// -// Copyright (c) 2025 owdax and The SwiftDevKit Contributors -// MIT License - https://opensource.org/licenses/MIT - -import Foundation -import Testing -@testable import SwiftDevKit - -@Suite("Truncate Debug Tests") -struct TruncateDebugTests { - @Test("Debug truncation") - func debugTruncation() throws { - let text = "This is a long text that needs to be truncated" - print("\nInput text: '\(text)'") - print("Input length: \(text.count)") - - // Test case 1: length: 20, smart: false - print("\n=== Test case 1: length: 20, smart: false ===") - let truncateAt1 = 20 - "...".count - print("Truncate at: \(truncateAt1)") - - let endIndex1 = text.index(text.startIndex, offsetBy: truncateAt1) - let truncated1 = String(text[.. Date: Mon, 20 Jan 2025 20:58:32 +0330 Subject: [PATCH 4/7] fix: Fix generic parameter inference in String+Extraction --- .../TextProcessing/String+Extraction.swift | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/Sources/SwiftDevKit/TextProcessing/String+Extraction.swift b/Sources/SwiftDevKit/TextProcessing/String+Extraction.swift index e1f65e7..14613f3 100644 --- a/Sources/SwiftDevKit/TextProcessing/String+Extraction.swift +++ b/Sources/SwiftDevKit/TextProcessing/String+Extraction.swift @@ -77,9 +77,9 @@ public extension String { let pattern = schemes .map { #"\b\#($0)://[^\s<>\"]+[\w]"# } .joined(separator: "|") - + guard let regex = try? NSRegularExpression(pattern: pattern) else { return [] } - + let range = NSRange(startIndex..., in: self) return regex.matches(in: self, range: range) .compactMap { match in @@ -100,7 +100,7 @@ public extension String { func extractEmails() -> [String] { let pattern = #"[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,64}"# guard let regex = try? NSRegularExpression(pattern: pattern) else { return [] } - + let range = NSRange(startIndex..., in: self) return regex.matches(in: self, range: range) .compactMap { match in @@ -122,7 +122,7 @@ public extension String { func extractHashtags(includeHash: Bool = true) -> [String] { let pattern = #"#[a-zA-Z][a-zA-Z0-9_]*"# guard let regex = try? NSRegularExpression(pattern: pattern) else { return [] } - + let range = NSRange(startIndex..., in: self) let matches = regex.matches(in: self, range: range) .compactMap { match in @@ -145,7 +145,7 @@ public extension String { func extractMentions(includeAt: Bool = true) -> [String] { let pattern = #"@[a-zA-Z][a-zA-Z0-9_]*"# guard let regex = try? NSRegularExpression(pattern: pattern) else { return [] } - + let range = NSRange(startIndex..., in: self) let matches = regex.matches(in: self, range: range) .compactMap { match in @@ -166,15 +166,15 @@ public extension String { /// - Returns: An array of strings containing the extracted valid dates func extractDates() -> [String] { let patterns = [ - #"\d{4}-\d{2}-\d{2}"#, // YYYY-MM-DD - #"\d{2}/\d{2}/\d{4}"#, // MM/DD/YYYY - #"\d{2}\.\d{2}\.\d{4}"#, // DD.MM.YYYY - #"[A-Za-z]+ \d{1,2}, \d{4}"# // Month DD, YYYY + #"\d{4}-\d{2}-\d{2}"#, // YYYY-MM-DD + #"\d{2}/\d{2}/\d{4}"#, // MM/DD/YYYY + #"\d{2}\.\d{2}\.\d{4}"#, // DD.MM.YYYY + #"[A-Za-z]+ \d{1,2}, \d{4}"#, // Month DD, YYYY ] - + let combinedPattern = patterns.joined(separator: "|") guard let regex = try? NSRegularExpression(pattern: combinedPattern) else { return [] } - + let range = NSRange(startIndex..., in: self) let dateFormatter = DateFormatter() dateFormatter.timeZone = TimeZone(identifier: "UTC") @@ -187,12 +187,12 @@ public extension String { .filter { dateString in // Try different date formats let formats = [ - "yyyy-MM-dd", // YYYY-MM-DD - "MM/dd/yyyy", // MM/DD/YYYY - "dd.MM.yyyy", // DD.MM.YYYY - "MMMM d, yyyy" // Month DD, YYYY + "yyyy-MM-dd", // YYYY-MM-DD + "MM/dd/yyyy", // MM/DD/YYYY + "dd.MM.yyyy", // DD.MM.YYYY + "MMMM d, yyyy", // Month DD, YYYY ] - + return formats.contains { format in dateFormatter.dateFormat = format return dateFormatter.date(from: dateString) != nil From 3783ff6dbd2b4cc70a3559693bf1b4a46c4ad1f1 Mon Sep 17 00:00:00 2001 From: owdax Date: Mon, 20 Jan 2025 22:17:08 +0330 Subject: [PATCH 5/7] fix: Fix generic parameter inference in String+Extraction --- Sources/SwiftDevKit/TextProcessing/String+Extraction.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/SwiftDevKit/TextProcessing/String+Extraction.swift b/Sources/SwiftDevKit/TextProcessing/String+Extraction.swift index 14613f3..cf4dcff 100644 --- a/Sources/SwiftDevKit/TextProcessing/String+Extraction.swift +++ b/Sources/SwiftDevKit/TextProcessing/String+Extraction.swift @@ -125,7 +125,7 @@ public extension String { let range = NSRange(startIndex..., in: self) let matches = regex.matches(in: self, range: range) - .compactMap { match in + .compactMap { (match: NSTextCheckingResult) -> String? in guard let range = Range(match.range, in: self) else { return nil } return String(self[range]) } @@ -148,7 +148,7 @@ public extension String { let range = NSRange(startIndex..., in: self) let matches = regex.matches(in: self, range: range) - .compactMap { match in + .compactMap { (match: NSTextCheckingResult) -> String? in guard let range = Range(match.range, in: self) else { return nil } return String(self[range]) } From 2db7937b1a74cce7f0d5aa680f9d394ab33cdc47 Mon Sep 17 00:00:00 2001 From: owdax Date: Mon, 20 Jan 2025 22:51:47 +0330 Subject: [PATCH 6/7] fix: Remove untested string distance metric functions --- .../TextProcessing/String+Metrics.swift | 173 ------------------ 1 file changed, 173 deletions(-) diff --git a/Sources/SwiftDevKit/TextProcessing/String+Metrics.swift b/Sources/SwiftDevKit/TextProcessing/String+Metrics.swift index 1d8b64a..c6961f9 100644 --- a/Sources/SwiftDevKit/TextProcessing/String+Metrics.swift +++ b/Sources/SwiftDevKit/TextProcessing/String+Metrics.swift @@ -6,179 +6,6 @@ import Foundation -public extension String { - /// Calculates the Levenshtein distance between this string and another string. - /// - /// The Levenshtein distance is the minimum number of single-character edits (insertions, deletions, or - /// substitutions) - /// required to change one string into another. - /// - /// Example: - /// ```swift - /// let distance = "kitten".levenshteinDistance(to: "sitting") // Returns 3 - /// ``` - /// - /// - Parameter other: The string to compare against - /// - Returns: The Levenshtein distance between the two strings - func levenshteinDistance(to other: String) -> Int { - let str1 = Array(self) - let str2 = Array(other) - let str1Length = str1.count - let str2Length = str2.count - - // Handle empty strings - if str1Length == 0 { return str2Length } - if str2Length == 0 { return str1Length } - - // Create matrix of size (str1Length+1)x(str2Length+1) - var matrix = Array(repeating: Array(repeating: 0, count: str2Length + 1), count: str1Length + 1) - - // Initialize first row and column - for i in 0...str1Length { - matrix[i][0] = i - } - for j in 0...str2Length { - matrix[0][j] = j - } - - // Fill in the rest of the matrix - for i in 1...str1Length { - for j in 1...str2Length { - if str1[i - 1] == str2[j - 1] { - matrix[i][j] = matrix[i - 1][j - 1] // No operation needed - } else { - matrix[i][j] = Swift.min( - matrix[i - 1][j] + 1, // deletion - matrix[i][j - 1] + 1, // insertion - matrix[i - 1][j - 1] + 1 // substitution - ) - } - } - } - - return matrix[str1Length][str2Length] - } - - /// Calculates the Jaro-Winkler distance between this string and another string. - /// - /// The Jaro-Winkler distance is a measure of similarity between two strings, with a higher score - /// indicating greater similarity (1.0 means the strings are identical, 0.0 means completely different). - /// It's particularly suited for short strings like names. - /// - /// Example: - /// ```swift - /// let similarity = "martha".jaroWinklerDistance(to: "marhta") // Returns ~0.961 - /// ``` - /// - /// - Parameters: - /// - other: The string to compare against - /// - scalingFactor: The scaling factor for how much the prefix affects the score (default: 0.1) - /// - Returns: A value between 0.0 and 1.0, where 1.0 means the strings are identical - func jaroWinklerDistance(to other: String, scalingFactor: Double = 0.1) -> Double { - let str1 = Array(self) - let str2 = Array(other) - let len1 = str1.count - let len2 = str2.count - - // If either string is empty, return 0 - if len1 == 0 || len2 == 0 { - return 0.0 - } - - // Maximum distance between two characters to be considered matching - let matchDistance = (Swift.max(len1, len2) / 2) - 1 - - // Arrays to keep track of matched characters - var matches1 = [Bool](repeating: false, count: len1) - var matches2 = [Bool](repeating: false, count: len2) - - // Count matching characters - var matchingChars = 0 - for i in 0.. 0 else { - return 0.0 - } - - // Count transpositions - var transpositions = 0 - var j = 0 - for i in 0.. Int { - guard count == other.count else { - throw StringMetricsError.unequalLength( - "Hamming distance requires strings of equal length: \(count) != \(other.count)") - } - - var distance = 0 - for i in 0.. Date: Mon, 20 Jan 2025 22:52:50 +0330 Subject: [PATCH 7/7] fix: Remove untested StringMetrics struct and file --- .../TextProcessing/String+Metrics.swift | 50 ------------------- 1 file changed, 50 deletions(-) delete mode 100644 Sources/SwiftDevKit/TextProcessing/String+Metrics.swift diff --git a/Sources/SwiftDevKit/TextProcessing/String+Metrics.swift b/Sources/SwiftDevKit/TextProcessing/String+Metrics.swift deleted file mode 100644 index c6961f9..0000000 --- a/Sources/SwiftDevKit/TextProcessing/String+Metrics.swift +++ /dev/null @@ -1,50 +0,0 @@ -// String+Metrics.swift -// SwiftDevKit -// -// Copyright (c) 2025 owdax and The SwiftDevKit Contributors -// MIT License - https://opensource.org/licenses/MIT - -import Foundation - -/// A collection of metrics describing a string's content. -public struct StringMetrics { - /// The total length of the string - public let totalLength: Int - - /// The number of words in the string - public let wordCount: Int - - /// The number of characters in the string - public let characterCount: Int - - /// The number of letters in the string - public let letterCount: Int - - /// The number of digits in the string - public let digitCount: Int - - /// The number of whitespace characters in the string - public let whitespaceCount: Int - - /// The number of punctuation characters in the string - public let punctuationCount: Int - - /// The number of symbol characters in the string - public let symbolCount: Int - - /// The number of lines in the string - public let lineCount: Int -} - -/// Errors that can occur during string metrics calculations. -public enum StringMetricsError: Error, LocalizedError { - /// Thrown when an operation requires strings of equal length but received strings of different lengths. - case unequalLength(String) - - public var errorDescription: String? { - switch self { - case let .unequalLength(message): - message - } - } -}