diff --git a/Package.swift b/Package.swift index ff62f58e..7c94b0a8 100644 --- a/Package.swift +++ b/Package.swift @@ -85,7 +85,7 @@ let package = Package( .target(name: "TIPagination", dependencies: ["Cursors", "TISwiftUtils"], path: "TIPagination/Sources"), .target(name: "TIAuth", dependencies: ["TIFoundationUtils", "TIUIKitCore", "KeychainAccess"], path: "TIAuth/Sources"), .target(name: "TIEcommerce", dependencies: ["TIFoundationUtils", "TISwiftUtils", "TINetworking", "TIUIKitCore", "TIUIElements"], path: "TIEcommerce/Sources"), - .target(name: "TITextProcessing", dependencies: ["Antlr4"], path: "TITextProcessing/Sources"), + .target(name: "TITextProcessing", dependencies: [.product(name: "Antlr4", package: "antlr4")], path: "TITextProcessing/Sources"), // MARK: - Tests @@ -93,5 +93,9 @@ let package = Package( name: "TITimerTests", dependencies: ["TIFoundationUtils"], path: "Tests/TITimerTests"), + .testTarget( + name: "TITextProcessingTests", + dependencies: ["TITextProcessing"], + path: "Tests/TITextProcessingTests") ] ) diff --git a/TITextProcessing/README.md b/TITextProcessing/README.md new file mode 100644 index 00000000..aa040abb --- /dev/null +++ b/TITextProcessing/README.md @@ -0,0 +1,102 @@ +# `TITextProcessing` + +### Библиотека для работы с регулярными выражениями + +## - `TextFormatter` + +Класс `TextFormatter` представляет из себя сервис, принимающий регулярное выражение на вход и предоставляющий возможность генерации следующих объектов:
+- `Replacement template` из `getRegexReplacement()`;
+- `Placeholder` из `getRegexPlaceholder()`;
+- `Formatter text` из `getFormattedText(_ text: String)` + +- +#### `func getRegexReplacement()` + +Метод, преобразующий входящее регулярное выражение в шаблон подстановки, например: + +**Input**: `(\\d{4}) ?(\\d{4}) ?(\\d{4}) ?(\\d{4})`
+**Output**: `$1 $2 $3 $4` + +- +#### `func getRegexPlaceholder()` + +Метод, преобразующий входящее регулярное выражение в текст-заполнитесь a.k.a placeholder, например: + +**Input**: `(\\d{4}) ?(\\d{4}) ?(\\d{4}) ?(\\d{4})`
+**Output**: `1234 5678 9012 3456` + +- +#### `func getFormattedText(_ text: String) -> String` + +Метод, преобразующий входящий текст к нужному формату, заранее определенному посредством указания регулярного выражения, например: + +**Input**: `2200111555550080`
+**Output**: `2200 1115 5555 0080` + +> P.S. Учитываем, что `TextFormatter` был проинициализирован со слеюущим регулярным выражением: `(\\d{4}) ?(\\d{4}) ?(\\d{4}) ?(\\d{4})` + +## - `RegexReplaceGenerator` + +Класс, отвечающий за генерацию `PCREGeneratorItem` из входящего регулярного выражения. Использует библиотеку `Antlr4` и `PCRE` для работы. + +- +#### `static func generateReplacement(for regex: String) -> PCREGeneratorItem` + +Функция, преобразующий входящее регулярное выражение в структуру, содержащую шаблон подстановки и матрицу символов, например: + +```swift +let item = RegexReplaceGenerator. generateReplacement(for: "(\\d{2})\\/?(\\d{2})") + +print(item.regexReplaceString) + +/* +Выведет в консоль: +"$1\\/$2" +*/ + +print(item.matrixOfSymbols) + +/* +Выведет в консоль: +[ + ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"], + ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"], + ["/"], + ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"], + ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"] +] +*/ +``` + +Итоговый `PCREGeneratorItem` содержит следующие данные: + +`regexReplaceString` - итоговый шаблон подстановки для изначального регулярного выражения;
+`matrixOfSymbols` - матрица символов, содержащая все возможные символы для каждого элемента в изначальном регулярном выражении + +## - `RegexPlaceholderGenerator` + +Класс, отвечающий за генерацию текста-заполнителя a.k.a placeholder. + +- +#### `static func generatePlaceholder(matrixOfSymbols: [[Character]]) -> String` + +Функция, преобразующая входящую матрицу символов в текст-заполнитель, например: + +```swift +let matrix: [[Character]] = [ + ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"], + ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"], + ["/"], + ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"], + ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"] +] + +let placeholder = RegexPlaceholderGenerator.generatePlaceholder(matrixOfSymbols: matrix) + +print(placeholder) + +/* +Выведет в консоль: +"12/34" +*/ +``` diff --git a/TITextProcessing/Sources/RegexPlaceholderGenerator/RegexPlaceholderGenerator.swift b/TITextProcessing/Sources/RegexPlaceholderGenerator/RegexPlaceholderGenerator.swift new file mode 100644 index 00000000..9aae3e8c --- /dev/null +++ b/TITextProcessing/Sources/RegexPlaceholderGenerator/RegexPlaceholderGenerator.swift @@ -0,0 +1,56 @@ +// +// Copyright (c) 2023 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +public final class RegexPlaceholderGenerator { + + public static func generatePlaceholder(matrixOfSymbols: [[Character]]) -> String { + var placeholderStringBuilder = String() + var indexes = [Array: Int]() + + matrixOfSymbols.forEach { listOfSymbols in + indexes[listOfSymbols] = 0 + } + + matrixOfSymbols.filter { !$0.isEmpty }.forEach { listOfSymbols in + if listOfSymbols.count == 1 { + placeholderStringBuilder.append(listOfSymbols[0]) + return + } + + if let index = indexes[listOfSymbols] { + var newIndex = index + + if listOfSymbols.count <= newIndex { + newIndex = 0 + } + + placeholderStringBuilder.append(listOfSymbols[newIndex]) + newIndex += 1 + indexes[listOfSymbols] = newIndex + } + } + + return placeholderStringBuilder + } +} diff --git a/TITextProcessing/Sources/RegexReplaceGenerator/PCREGeneratorItem.swift b/TITextProcessing/Sources/RegexReplaceGenerator/PCREGeneratorItem.swift new file mode 100644 index 00000000..e13c85e4 --- /dev/null +++ b/TITextProcessing/Sources/RegexReplaceGenerator/PCREGeneratorItem.swift @@ -0,0 +1,28 @@ +// +// Copyright (c) 2023 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +public struct PCREGeneratorItem { + public let regexReplaceString: String + public let matrixOfSymbols: [[Character]] +} diff --git a/TITextProcessing/Sources/RegexReplaceGenerator/PCREGeneratorListener.swift b/TITextProcessing/Sources/RegexReplaceGenerator/PCREGeneratorListener.swift new file mode 100644 index 00000000..d6991fa2 --- /dev/null +++ b/TITextProcessing/Sources/RegexReplaceGenerator/PCREGeneratorListener.swift @@ -0,0 +1,172 @@ +// +// Copyright (c) 2023 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation +import Antlr4 + +public final class PCREGeneratorListener: PCREBaseListener { + + // MARK: - Properties + + /* + Matrix of available symbols for placeholder where + index - symbol number and value - all available symbols + */ + private var matrixOfSymbols = [[Character]]() + + /* + Regex group index counter, 1 by default + */ + private var currentGroupIndex = 1 + + /* + The final output replacement of the entered regex + */ + private var regexReplaceString = "" + + /* + Search element from the regex + Could contain possible elements of a regex, e.g.: + [1-2], \\d, [A-B] and elements not related to regex or escaped + */ + private var listOfSymbols = [Character]() + + // MARK: - Overrides + + /* + Called when a new capture group found + */ + public override func enterCapture(_ ctx: PCREParser.CaptureContext) { + super.enterCapture(ctx) + + regexReplaceString += "$\(currentGroupIndex)" + currentGroupIndex += 1 + } + + /* + Called when there is a digit symbol found, e.g.: + \d{2} where \d is an indication of a digit symbol + */ + public override func enterShared_atom(_ ctx: PCREParser.Shared_atomContext) { + super.enterShared_atom(ctx) + + listOfSymbols = "1234567890".map { Character(String($0)) } + matrixOfSymbols.append(listOfSymbols) + } + + /* + Called when there is a range found, e.g.: + [А-дD-f] or [А-д] + */ + public override func enterCharacter_class(_ ctx: PCREParser.Character_classContext) { + super.enterCharacter_class(ctx) + + // Range count validation + // - true if [А-дD-f] + // - false if [А-д] + if ctx.cc_atom().count > 1 { + listOfSymbols = [] + + guard let firstChar = ctx.CharacterClassStart()?.getText() else { + listOfSymbols = getAvailableSymbols(for: ctx.getText()) + return + } + + let endChar = ctx.CharacterClassEnd()[0].getText() + + for i in 0 ..< ctx.cc_atom().count { + listOfSymbols += getAvailableSymbols(for: firstChar + ctx.cc_atom()[i].getText() + endChar) + } + } else { + listOfSymbols = getAvailableSymbols(for: ctx.getText()) + } + + matrixOfSymbols.append(listOfSymbols) + } + + /* + Called when there is a number of element duplication found, e.g.: + [A-B]{6} where {6} is a number of required element duplication + */ + public override func enterDigits(_ ctx: PCREParser.DigitsContext) { + super.enterDigits(ctx) + + guard let count = Int(ctx.getText()) else { + return + } + + for _ in 1 ..< count { + matrixOfSymbols.append(listOfSymbols) + } + } + + /* + Called when there is a single non-group literal found, e.g.: + (?:\\+7 ) where "+", "7" and " " are single non-group literals + */ + public override func enterLiteral(_ ctx: PCREParser.LiteralContext) { + super.enterLiteral(ctx) + + guard let text = ctx.shared_literal()?.getText() else { + return + } + + regexReplaceString += text + listOfSymbols = [] + + ctx.getText().forEach { symbol in + listOfSymbols.append(symbol) + } + + matrixOfSymbols.append(listOfSymbols) + } + + // MARK: - Public methods + + public func toPCREGeneratorItem() -> PCREGeneratorItem { + return PCREGeneratorItem(regexReplaceString: regexReplaceString, + matrixOfSymbols: matrixOfSymbols.map { $0.filter { $0 != "\\" } }) + } + + // MARK: - Private methods + + private func getAvailableSymbols(for ctxText: String) -> [Character] { + let startAtomStr = ctxText[ctxText.index(after: ctxText.startIndex)] + let endAtomStr = ctxText[ctxText.index(ctxText.endIndex, offsetBy: -2)] + + guard (startAtomStr.isLetter || startAtomStr.isNumber) && (endAtomStr.isLetter || endAtomStr.isNumber) else { + return [startAtomStr, endAtomStr] + } + + guard let startRangeScalar = startAtomStr.unicodeScalars.first?.value, + let endRangeScalar = endAtomStr.unicodeScalars.first?.value else { + return [startAtomStr, endAtomStr] + } + + let symbols = (startRangeScalar...endRangeScalar) + .compactMap(UnicodeScalar.init) + .map(Character.init) + .filter { $0.isLetter || $0.isNumber } + + return symbols + } +} diff --git a/TITextProcessing/Sources/RegexReplaceGenerator/RegexReplaceGenerator.swift b/TITextProcessing/Sources/RegexReplaceGenerator/RegexReplaceGenerator.swift new file mode 100644 index 00000000..bcd0095a --- /dev/null +++ b/TITextProcessing/Sources/RegexReplaceGenerator/RegexReplaceGenerator.swift @@ -0,0 +1,45 @@ +// +// Copyright (c) 2023 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation +import Antlr4 + +public final class RegexReplaceGenerator { + + public static func generateReplacement(for regex: String) -> PCREGeneratorItem { + let inputStream = ANTLRInputStream(regex) + let lexer = PCRELexer(inputStream) + let tokens = CommonTokenStream(lexer) + let walker = ParseTreeWalker() + let pcreGeneratorListener = PCREGeneratorListener() + + let parser = try? PCREParser(tokens) + + guard let parseContext = try? parser?.parse() as? ParseTree else { + fatalError("Cannot parse input regex") + } + + try? walker.walk(pcreGeneratorListener, parseContext) + + return pcreGeneratorListener.toPCREGeneratorItem() + } +} diff --git a/TITextProcessing/Sources/TextFormatter/TextFormatter.swift b/TITextProcessing/Sources/TextFormatter/TextFormatter.swift new file mode 100644 index 00000000..94badf2b --- /dev/null +++ b/TITextProcessing/Sources/TextFormatter/TextFormatter.swift @@ -0,0 +1,53 @@ +// +// Copyright (c) 2023 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +public final class TextFormatter { + + private let regex: String + + public init(regex: String) { + self.regex = regex + } + + public func getRegexReplacement() -> String { + RegexReplaceGenerator.generateReplacement(for: regex).regexReplaceString + } + + public func getRegexPlaceholder() -> String { + let matrixOfSymbols = RegexReplaceGenerator.generateReplacement(for: regex).matrixOfSymbols + + return RegexPlaceholderGenerator.generatePlaceholder(matrixOfSymbols: matrixOfSymbols) + } + + public func getFormattedText(_ text: String) -> String { + guard let expression = try? NSRegularExpression(pattern: regex, options: .caseInsensitive) else { + fatalError("Cannot create NSRegularExpression from input regex") + } + + return expression.stringByReplacingMatches(in: text, + options: .reportProgress, + range: NSMakeRange(0, text.count), + withTemplate: getRegexReplacement()) + } +} diff --git a/Tests/TITextProcessingTests/TITextProcessingTests.swift b/Tests/TITextProcessingTests/TITextProcessingTests.swift new file mode 100644 index 00000000..ef7e35d3 --- /dev/null +++ b/Tests/TITextProcessingTests/TITextProcessingTests.swift @@ -0,0 +1,112 @@ +// +// Copyright (c) 2023 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import XCTest +@testable import TITextProcessing + +final class TITextProcessingTests: XCTestCase { + + func testDateRegex() { + // given + let regex = "(\\d{2})\\/?(\\d{2})" + let inputText = "1525" + let formatter = TextFormatter(regex: regex) + + // when + let regexReplacement = formatter.getRegexReplacement() + let regexPlaceholder = formatter.getRegexPlaceholder() + let formattedText = formatter.getFormattedText(inputText) + + // then + XCTAssertEqual(regexReplacement, "$1\\/$2") + XCTAssertEqual(regexPlaceholder, "12/34") + XCTAssertEqual(formattedText, "15/25") + } + + func testCardNumberRegex() { + // given + let regex = "(\\d{4}) ?(\\d{4}) ?(\\d{4}) ?(\\d{4})" + let inputText = "2200111555550080" + let formatter = TextFormatter(regex: regex) + + // when + let regexReplacement = formatter.getRegexReplacement() + let regexPlaceholder = formatter.getRegexPlaceholder() + let formattedText = formatter.getFormattedText(inputText) + + // then + XCTAssertEqual(regexReplacement, "$1 $2 $3 $4") + XCTAssertEqual(regexPlaceholder, "1234 5678 9012 3456") + XCTAssertEqual(formattedText, "2200 1115 5555 0080") + } + + func testPhoneNumberRegex() { + // given + let regex = "(?:\\+7 )?\\(?(\\d{3})\\)? ?(\\d{3}) ?(\\d{2}) ?(\\d{2})" + let inputText = "9995534820" + let formatter = TextFormatter(regex: regex) + + // when + let regexReplacement = formatter.getRegexReplacement() + let regexPlaceholder = formatter.getRegexPlaceholder() + let formattedText = formatter.getFormattedText(inputText) + + // then + XCTAssertEqual(regexReplacement, "\\+7 \\($1\\) $2 $3 $4") + XCTAssertEqual(regexPlaceholder, "+7 (123) 456 78 90") + XCTAssertEqual(formattedText, "+7 (999) 553 48 20") + } + + func testBirthdayCertificateRegex() { + // given + let regex = "([A-Z]{2})-?([А-Я]{2}) ?№? ?(\\d{6})" + let inputText = "ABЮЯ689323" + let formatter = TextFormatter(regex: regex) + + // when + let regexReplacement = formatter.getRegexReplacement() + let regexPlaceholder = formatter.getRegexPlaceholder() + let formattedText = formatter.getFormattedText(inputText) + + // then + XCTAssertEqual(regexReplacement, "$1-$2 № $3") + XCTAssertEqual(regexPlaceholder, "AB-АБ № 123456") + XCTAssertEqual(formattedText, "AB-ЮЯ № 689323") + } + + func testRoubleSumRegex() { + // given + let regex = "(\\d+)([.,]\\d+)? ?₽?" + let inputText = "1234.56" + let formatter = TextFormatter(regex: regex) + + // when + let regexReplacement = formatter.getRegexReplacement() + let regexPlaceholder = formatter.getRegexPlaceholder() + let formattedText = formatter.getFormattedText(inputText) + + // then + XCTAssertEqual(regexReplacement, "$1$2 ₽") + XCTAssertEqual(regexPlaceholder, "1.2 ₽") + XCTAssertEqual(formattedText, "1234.56 ₽") + } +}