Created
March 31, 2017 23:30
-
-
Save insidegui/1599c21ab6be9d866219a6f6f27eea9e to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// MaskPattern.swift | |
// PeixeUrbano | |
// | |
// Created by Guilherme Rambo on 24/01/17. | |
// | |
// | |
import Foundation | |
public struct MaskPattern { | |
public typealias FilterClosure = (String) -> String | |
public typealias ConditionClosure = (String) -> Bool | |
/// A filter that can be used to remove unwanted characters from the input | |
public struct Filter { | |
public let closure: FilterClosure | |
} | |
/// A condition that can be used to determine whether an alternate pattern should be used | |
public struct Condition { | |
public let closure: ConditionClosure | |
} | |
/// The character to be replaced by the actual values from the input (ex: *) | |
public let token: Character | |
/// The pattern to be applied to the input (ex: ***.***-***) | |
public let pattern: String | |
/// Filter to apply to the input before applying the mask | |
public let filter: Filter? | |
/// Alternate pattern to be used when alternateCondition returns true | |
public let alternate: String? | |
/// Condition to check whether the alternate pattern should be used | |
public let alternateCondition: Condition? | |
/// MaskPattern initializer | |
/// | |
/// - Parameters: | |
/// - token: The character to be replaced by the actual values from the input (ex: *) | |
/// - pattern: The pattern to be applied to the input (ex: ***.***-***) | |
/// - filter: Filter to apply to the input before applying the mask | |
public init(token: Character, | |
pattern: String, | |
filter: Filter?) { | |
self.token = token | |
self.pattern = pattern | |
self.filter = filter | |
self.alternate = nil | |
self.alternateCondition = nil | |
} | |
/// MaskPattern initializer with alternate pattern | |
/// | |
/// - Parameters: | |
/// - token: The character to be replaced by the actual values from the input (ex: *) | |
/// - pattern: The pattern to be applied to the input (ex: ***.***-***) | |
/// - filter: Filter to apply to the input before applying the mask | |
/// - alternate: Alternate pattern to be used when alternateCondition returns true | |
/// - alternateCondition: Condition to check whether the alternate pattern should be used | |
public init(token: Character, | |
pattern: String, | |
filter: Filter?, | |
alternate: String, | |
alternateCondition: Condition) { | |
self.token = token | |
self.pattern = pattern | |
self.filter = filter | |
self.alternate = alternate | |
self.alternateCondition = alternateCondition | |
} | |
} | |
extension MaskPattern.Filter { | |
/// Removes non-digit characters | |
public static let digitsOnly = MaskPattern.Filter { $0.components(separatedBy: CharacterSet.decimalDigits.inverted).joined() } | |
} | |
extension MaskPattern.Condition { | |
/// Returns true if input looks like a landline phone number as opposed to a cell number | |
public static let landlinePhone = MaskPattern.Condition { input in | |
guard let firstChar = input.characters.first else { return false } | |
return String(firstChar) != "9" | |
} | |
/// Returns true if input looks like a landline phone number including area code as opposed to a cell number | |
public static let landlinePhoneWithArea = MaskPattern.Condition { input in | |
guard input.characters.count >= 3 else { return false } | |
return input[2] != "9" | |
} | |
} | |
extension MaskPattern { | |
public static let cpf = MaskPattern(token: "*", | |
pattern: "***.***.***-**", | |
filter: .digitsOnly) | |
public static let zipCode = MaskPattern(token: "*", | |
pattern: "*****-***", | |
filter: .digitsOnly) | |
public static let creditCard = MaskPattern(token: "*", | |
pattern: "**** **** **** ****", | |
filter: .digitsOnly) | |
public static let phoneArea = MaskPattern(token: "*", | |
pattern: "**", | |
filter: .digitsOnly) | |
public static let phoneNumber = MaskPattern(token: "*", | |
pattern: "* **** ****", | |
filter: .digitsOnly, | |
alternate: "**** ****", | |
alternateCondition: .landlinePhone) | |
public static let fullPhoneNumber = MaskPattern(token: "*", | |
pattern: "(**) * **** ****", | |
filter: .digitsOnly, | |
alternate: "(**) **** ****", | |
alternateCondition: .landlinePhoneWithArea) | |
public static let cardExpirationDate = MaskPattern(token: "*", | |
pattern: "**/**", | |
filter: .digitsOnly) | |
public static let cvv = MaskPattern(token: "*", | |
pattern: "****", | |
filter: .digitsOnly) | |
public static let houseNumber = MaskPattern(token: "*", | |
pattern: "**********", | |
filter: .digitsOnly) | |
} | |
extension MaskPattern { | |
func apply(to input: String) -> String { | |
let filteredInput = filter?.closure(input) ?? input | |
guard filteredInput.characters.count > 0 else { return filteredInput } | |
// the actual pattern to be applied | |
var targetPattern = pattern | |
// check whether we should use the alternate pattern instead of the default one | |
if let condition = alternateCondition, | |
let altPattern = alternate { | |
if condition.closure(filteredInput) { | |
targetPattern = altPattern | |
} | |
} | |
let inputLength = filteredInput.characters.count | |
let patternLength = targetPattern.characters.count | |
var finished = false | |
var inputIndex = 0 | |
var patternIndex = 0 | |
var output = "" | |
repeat { | |
let patternChar = targetPattern[patternIndex] | |
let inputChar = filteredInput[inputIndex] | |
if patternChar == String(token) { | |
output += inputChar | |
inputIndex += 1 | |
} else { | |
output += patternChar | |
} | |
patternIndex += 1 | |
finished = patternIndex >= patternLength || inputIndex >= inputLength | |
} while !finished | |
return output | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment