Skip to content

Instantly share code, notes, and snippets.

@insidegui
Created March 31, 2017 23:30
Show Gist options
  • Save insidegui/1599c21ab6be9d866219a6f6f27eea9e to your computer and use it in GitHub Desktop.
Save insidegui/1599c21ab6be9d866219a6f6f27eea9e to your computer and use it in GitHub Desktop.
//
// 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