Skip to content

Instantly share code, notes, and snippets.

@zaimramlan
Created October 27, 2018 22:18
Show Gist options
  • Save zaimramlan/2597b024162a88acb8db90fdb99b0fbe to your computer and use it in GitHub Desktop.
Save zaimramlan/2597b024162a88acb8db90fdb99b0fbe to your computer and use it in GitHub Desktop.
Swift's String(format:arguments:) method (peeking under the hood)
extension String {
/// Replaces format specifiers (with index) with given arguments.
/// (Only works if given arguments are Strings and to replace Strings)
///
/// - Parameter arguments: List of String replacements.
/// - Returns: This String replaced with given Strings (unless arguments are not a String array)
func replaceFormatSpecifiersWithIndex(with arguments: CVarArg...) -> String {
guard let args = arguments as? [String] else { return self }
// ensure patterns are valid
let formatSpecifierPattern = "%[\\d]+\\$@"
let digitPattern = "[\\d]+"
guard
let formatSpecifierRegEx = try? NSRegularExpression(pattern: formatSpecifierPattern, options: .useUnixLineSeparators),
let digitRegEx = try? NSRegularExpression(pattern: digitPattern, options: .useUnixLineSeparators)
else { return self }
// extract range of format specifiers (if any)
var range = NSRange(location: 0, length: self.count)
var matches = formatSpecifierRegEx.matches(in: self, options: .withoutAnchoringBounds, range: range)
let mutableSelf = NSMutableString(string: self)
while matches.count > 0 {
// extract index from format specifier
guard let matchingFormatSpecifierRange = matches.first?.range else { break }
let formatSpecifier = mutableSelf.substring(with: matchingFormatSpecifierRange)
let digitRange = NSRange(location: 0, length: formatSpecifier.count)
let digitMatches = digitRegEx.matches(in: formatSpecifier, options: .withoutAnchoringBounds, range: digitRange)
guard let matchingDigitRange = digitMatches.first?.range else { break }
let mutableFormatSpecifier = NSMutableString(string: formatSpecifier)
let index = NSDecimalNumber(string: mutableFormatSpecifier.substring(with: matchingDigitRange)).intValue - 1
// try to replace string
let replacementString = index < args.count ? args[index] : "(null)"
mutableSelf.replaceCharacters(in: matchingFormatSpecifierRange, with: replacementString)
// overwrite range & matches to reflect newly replaced string
range = NSRange(location: 0, length: mutableSelf.description.count)
matches = formatSpecifierRegEx.matches(in: mutableSelf.description, options: .withoutAnchoringBounds, range: range)
}
return mutableSelf.description
}
/// Replaces format specifiers (without index) with given arguments.
/// (Only works if given arguments are Strings and to replace Strings)
///
/// - Parameter arguments: List of String replacements.
/// - Returns: This String replaced with given Strings (unless arguments are not a String array)
func replaceFormatSpecifierWithoutIndex(with arguments: CVarArg...) -> String {
guard let args = arguments as? [String] else { return self }
// ensure pattern is valid
let formatSpecifierPattern = "%@"
guard let regEx = try? NSRegularExpression(pattern: formatSpecifierPattern, options: .useUnixLineSeparators) else { return self }
// extract range of format specifiers (if any)
var range = NSRange(location: 0, length: self.count)
var matches = regEx.matches(in: self, options: .withoutAnchoringBounds, range: range)
let mutableSelf = NSMutableString(string: self)
var index = 0
while matches.count > 0 {
// try to replace string
guard let matchingFormatSpecifierRange = matches.first?.range else { break }
let replacementString = index < args.count ? args[index] : "(null)"
mutableSelf.replaceCharacters(in: matchingFormatSpecifierRange, with: replacementString)
// overwrite range & matches to reflect newly replaced string
range = NSRange(location: 0, length: mutableSelf.description.count)
matches = regEx.matches(in: mutableSelf.description, options: .withoutAnchoringBounds, range: range)
index += 1
}
return mutableSelf.description
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment