Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 32 additions & 25 deletions Sources/TranslationCatalogIO/Extensions/String+IO.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import Foundation

extension String {
private static let darwinStringReplacement = "%@"
private static let posixStringReplacement = "%s"
/// Regex pattern for inline substitutions.
///
/// Matches:
/// * **token**: Starts with `%`
/// * **position** Optional positional indication `1$`
/// * **type** Ends with one of `s` (string), `ld` (int), `llu` (unsigned int), `lf` (float), `@` (Darwin object)
private static let substitutionPattern = #"(?<token>%)(?<position>\d+\$)?(?<type>s|ld|llu|lf|@)"#
private static let darwinStringToken = "@"
private static let posixStringToken = "s"

func simpleAppleDictionaryEscaped() -> String {
let replacements: [(Character, String)] = [
Expand Down Expand Up @@ -38,41 +45,41 @@ extension String {
return updated
}

var hasMultipleReplacements: Bool {
(numberOfInstances("%@") > 1) || (numberOfInstances("%s") > 1)
var substitutionRanges: [Range<Self.Index>] {
get throws {
let regex = try Regex(Self.substitutionPattern)
return ranges(of: regex)
}
}

func numberOfInstances(_ substring: String) -> Int {
guard !isEmpty else {
return 0
var hasMultipleReplacements: Bool {
do {
return try substitutionRanges.count > 1
} catch {
return false
}
}

var count = 0
func replaceToken(_ token: String, with replacement: String) throws -> String {
let ranges = try substitutionRanges
var modified = self

var range: Range<String.Index>?
while let match = self.range(of: substring, options: [], range: range) {
count += 1
range = Range(uncheckedBounds: (lower: match.upperBound, upper: endIndex))
for range in ranges.reversed() {
let substring = modified[range]
if substring.hasSuffix(token) {
let lowerBound = modified.index(range.upperBound, offsetBy: -token.count)
modified.replaceSubrange(lowerBound ..< range.upperBound, with: replacement)
}
}

return count
return modified
}

func decodingDarwinStrings() throws -> String {
try replaceIn(self, pattern: Self.darwinStringReplacement, with: Self.posixStringReplacement)
try replaceToken(Self.darwinStringToken, with: Self.posixStringToken)
}

func encodingDarwinStrings() throws -> String {
try replaceIn(self, pattern: Self.posixStringReplacement, with: Self.darwinStringReplacement)
}

private func replaceIn(_ value: String, pattern: String, with: String) throws -> String {
let regex = try Regex(pattern)
var output = value
let ranges = value.ranges(of: regex).reversed()
for range in ranges {
output.replaceSubrange(range, with: with)
}
return output
try replaceToken(Self.posixStringToken, with: Self.darwinStringToken)
}
}
70 changes: 68 additions & 2 deletions Tests/TranslationCatalogTests/StringReplacementTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,77 @@ final class StringReplacementTests: XCTestCase {

func testDecodeDarwinStrings() throws {
XCTAssertEqual(try "Hello %@".decodingDarwinStrings(), "Hello %s")
XCTAssertEqual(try "Hello $1%@".decodingDarwinStrings(), "Hello $1%s")
XCTAssertEqual(try "Hello %1$@".decodingDarwinStrings(), "Hello %1$s")
}

func testEncodeDarwinStrings() throws {
XCTAssertEqual(try "Hello %s".encodingDarwinStrings(), "Hello %@")
XCTAssertEqual(try "Hello $1%s".encodingDarwinStrings(), "Hello $1%@")
XCTAssertEqual(try "Hello %1$s".encodingDarwinStrings(), "Hello %1$@")
}

func testSubstitutionDetection() throws {
var string = "Posix String %s Replacement"
var ranges = try string.substitutionRanges
XCTAssertEqual(ranges.count, 1)
XCTAssertEqual(ranges.first?.lowerBound, string.index(string.startIndex, offsetBy: 13))
XCTAssertEqual(ranges.first?.upperBound, string.index(string.startIndex, offsetBy: 15))

string = "Posix String %1$s Replacement"
ranges = try string.substitutionRanges
XCTAssertEqual(ranges.count, 1)
XCTAssertEqual(ranges.first?.lowerBound, string.index(string.startIndex, offsetBy: 13))
XCTAssertEqual(ranges.first?.upperBound, string.index(string.startIndex, offsetBy: 17))

string = "Darwin String %@ Replacement"
ranges = try string.substitutionRanges
XCTAssertEqual(ranges.count, 1)
XCTAssertEqual(ranges.first?.lowerBound, string.index(string.startIndex, offsetBy: 14))
XCTAssertEqual(ranges.first?.upperBound, string.index(string.startIndex, offsetBy: 16))

string = "Darwin String %1$@ Replacement"
ranges = try string.substitutionRanges
XCTAssertEqual(ranges.count, 1)
XCTAssertEqual(ranges.first?.lowerBound, string.index(string.startIndex, offsetBy: 14))
XCTAssertEqual(ranges.first?.upperBound, string.index(string.startIndex, offsetBy: 18))

string = "Posix Int %ld Replacement"
ranges = try string.substitutionRanges
XCTAssertEqual(ranges.count, 1)
XCTAssertEqual(ranges.first?.lowerBound, string.index(string.startIndex, offsetBy: 10))
XCTAssertEqual(ranges.first?.upperBound, string.index(string.startIndex, offsetBy: 13))

string = "Posix Int %1$ld Replacement"
ranges = try string.substitutionRanges
XCTAssertEqual(ranges.count, 1)
XCTAssertEqual(ranges.first?.lowerBound, string.index(string.startIndex, offsetBy: 10))
XCTAssertEqual(ranges.first?.upperBound, string.index(string.startIndex, offsetBy: 15))

string = "Posix Unsigned-Int %llu Replacement"
ranges = try string.substitutionRanges
XCTAssertEqual(ranges.count, 1)
XCTAssertEqual(ranges.first?.lowerBound, string.index(string.startIndex, offsetBy: 19))
XCTAssertEqual(ranges.first?.upperBound, string.index(string.startIndex, offsetBy: 23))

string = "Posix Unsigned-Int %1$llu Replacement"
ranges = try string.substitutionRanges
XCTAssertEqual(ranges.count, 1)
XCTAssertEqual(ranges.first?.lowerBound, string.index(string.startIndex, offsetBy: 19))
XCTAssertEqual(ranges.first?.upperBound, string.index(string.startIndex, offsetBy: 25))

string = "Posix Float %lf Replacement"
ranges = try string.substitutionRanges
XCTAssertEqual(ranges.count, 1)
XCTAssertEqual(ranges.first?.lowerBound, string.index(string.startIndex, offsetBy: 12))
XCTAssertEqual(ranges.first?.upperBound, string.index(string.startIndex, offsetBy: 15))

string = "Posix Float %1$lf Replacement"
ranges = try string.substitutionRanges
XCTAssertEqual(ranges.count, 1)
XCTAssertEqual(ranges.first?.lowerBound, string.index(string.startIndex, offsetBy: 12))
XCTAssertEqual(ranges.first?.upperBound, string.index(string.startIndex, offsetBy: 17))

string = "Multi-match %s. Int %ld, could equal %llu, but does not equal %lf."
ranges = try string.substitutionRanges
XCTAssertEqual(ranges.count, 4)
}
}