You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
395 lines
14 KiB
395 lines
14 KiB
//
|
|
// String+Extensions.swift
|
|
// PadelClub
|
|
//
|
|
// Created by Razmig Sarkissian on 01/03/2024.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
// MARK: - Trimming and stuff
|
|
|
|
public extension String {
|
|
func trunc(length: Int, trailing: String = "…") -> String {
|
|
if length <= 0 { return self }
|
|
return (self.count > length) ? self.prefix(length) + trailing : self
|
|
}
|
|
|
|
func prefixTrimmed(_ length: Int) -> String {
|
|
String(trimmed.prefix(length))
|
|
}
|
|
|
|
func prefixMultilineTrimmed(_ length: Int) -> String {
|
|
String(trimmedMultiline.prefix(length))
|
|
}
|
|
|
|
var trimmed: String {
|
|
replaceCharactersFromSet(characterSet: .newlines, replacementString: " ").trimmingCharacters(in: .whitespacesAndNewlines)
|
|
}
|
|
|
|
var trimmedMultiline: String {
|
|
self.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
}
|
|
|
|
func replaceCharactersFromSet(characterSet: CharacterSet, replacementString: String = "") -> String {
|
|
components(separatedBy: characterSet).joined(separator:replacementString)
|
|
}
|
|
|
|
var canonicalVersion: String {
|
|
trimmed.replaceCharactersFromSet(characterSet: .punctuationCharacters, replacementString: " ").folding(options: .diacriticInsensitive, locale: .current).lowercased()
|
|
}
|
|
|
|
var canonicalVersionWithPunctuation: String {
|
|
trimmed.folding(options: .diacriticInsensitive, locale: .current).lowercased()
|
|
}
|
|
|
|
var removingFirstCharacter: String {
|
|
String(dropFirst())
|
|
}
|
|
|
|
func isValidEmail() -> Bool {
|
|
let emailRegEx = "^[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}$"
|
|
let emailPredicate = NSPredicate(format:"SELF MATCHES %@", emailRegEx)
|
|
return emailPredicate.evaluate(with: self)
|
|
}
|
|
|
|
}
|
|
|
|
// MARK: - Club Name
|
|
public extension String {
|
|
func acronym() -> String {
|
|
let acronym = canonicalVersion.replaceCharactersFromSet(characterSet: .whitespacesAndNewlines)
|
|
if acronym.count > 10 {
|
|
return concatenateFirstLetters().uppercased()
|
|
} else {
|
|
return acronym.uppercased()
|
|
}
|
|
}
|
|
|
|
func concatenateFirstLetters() -> String {
|
|
// Split the input into sentences
|
|
let sentences = self.components(separatedBy: .whitespacesAndNewlines)
|
|
if sentences.count == 1 {
|
|
return String(self.prefix(10))
|
|
}
|
|
// Extract the first character of each sentence
|
|
let firstLetters = sentences.compactMap { sentence -> Character? in
|
|
let trimmedSentence = sentence.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
if trimmedSentence.count > 2 {
|
|
if let firstCharacter = trimmedSentence.first {
|
|
return firstCharacter
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Join the first letters together into a string
|
|
let result = String(firstLetters)
|
|
return String(result.prefix(10))
|
|
}
|
|
}
|
|
|
|
// MARK: - FFT License
|
|
public extension String {
|
|
var computedLicense: String {
|
|
if let licenseKey {
|
|
return self + licenseKey
|
|
} else {
|
|
return self
|
|
}
|
|
}
|
|
|
|
var strippedLicense: String? {
|
|
var dropFirst = 0
|
|
if hasPrefix("0") {
|
|
dropFirst = 1
|
|
}
|
|
if let match = self.dropFirst(dropFirst).firstMatch(of: /[0-9]{6,8}/) {
|
|
let lic = String(self.dropFirst(dropFirst)[match.range.lowerBound..<match.range.upperBound])
|
|
return lic
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
var isLicenseNumber: Bool {
|
|
if let match = self.firstMatch(of: /[0-9]{6,8}[A-Z]/) {
|
|
let lic = String(self[match.range.lowerBound..<match.range.upperBound].dropLast(1))
|
|
let lastLetter = String(self[match.range.lowerBound..<match.range.upperBound].suffix(1))
|
|
|
|
if let lkey = lic.licenseKey {
|
|
return lkey == lastLetter
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
var licenseKey: String? {
|
|
if let intValue = Int(self) {
|
|
var value = intValue
|
|
value -= 1
|
|
value = value % 23
|
|
let v = UnicodeScalar("A").value
|
|
let i = Int(v)
|
|
if let s = UnicodeScalar(i + value) {
|
|
var c = Character(s)
|
|
if c >= "I" {
|
|
value += 1
|
|
if let newS = UnicodeScalar(i + value) {
|
|
c = Character(newS)
|
|
}
|
|
}
|
|
|
|
if c >= "O" {
|
|
value += 1
|
|
if let newS = UnicodeScalar(i + value) {
|
|
c = Character(newS)
|
|
}
|
|
}
|
|
|
|
|
|
if c >= "Q" {
|
|
value += 1
|
|
if let newS = UnicodeScalar(i + value) {
|
|
c = Character(newS)
|
|
}
|
|
}
|
|
|
|
return String(c)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func licencesFound() -> [String] {
|
|
// First try to find licenses with format: 5-8 digits followed by optional letter
|
|
let precisePattern = /[1-9][0-9]{5,7}[ ]?[A-Za-z]?/
|
|
let preciseMatches = self.matches(of: precisePattern)
|
|
let preciseResults = preciseMatches.map { String(self[$0.range]).trimmingCharacters(in: .whitespaces) }
|
|
|
|
// If we find potential licenses with the precise pattern
|
|
if !preciseResults.isEmpty {
|
|
// Filter to only include those with trailing letters
|
|
let licensesWithLetters = preciseResults.filter {
|
|
let lastChar = $0.last
|
|
return lastChar != nil && lastChar!.isLetter
|
|
}
|
|
|
|
print("🎫 Found \(preciseResults.count) potential licenses, filtering to \(licensesWithLetters.count) with trailing letters")
|
|
|
|
// If we have licenses with letters, validate them
|
|
if !licensesWithLetters.isEmpty {
|
|
let validLicenses = licensesWithLetters.filter { $0.isLicenseNumber }
|
|
|
|
// If we have valid licenses, return the numeric part of each
|
|
if !validLicenses.isEmpty {
|
|
let numericLicenses = validLicenses.map { license -> String in
|
|
// Extract just the numeric part (all characters except the last letter)
|
|
if let lastChar = license.last, lastChar.isLetter {
|
|
return String(license.dropLast())
|
|
}
|
|
return license
|
|
}
|
|
|
|
if numericLicenses.isEmpty == false {
|
|
print("🎫 Found valid licenses: \(validLicenses), returning numeric parts: \(numericLicenses)")
|
|
return numericLicenses
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback to just number pattern if we didn't find good matches
|
|
let numberPattern = /[1-9][0-9]{5,7}/
|
|
let numberMatches = self.matches(of: numberPattern)
|
|
let numberResults = numberMatches.map { String(self[$0.range]) }
|
|
|
|
print("🎫 Falling back to number-only pattern, found: \(numberResults)")
|
|
return numberResults
|
|
}
|
|
}
|
|
|
|
// MARK: - FFT Source Importing
|
|
public extension String {
|
|
enum RegexStatic {
|
|
// Patterns for France only
|
|
static let phoneNumber = /^(\+33|0033|33|0)[1-9][0-9]{8}$/
|
|
static let phoneNumberWithExtra0 = /^33[0][1-9][0-9]{8}$/
|
|
static let mobileNumber = /^(\+33|0033|33|0)[6-7][0-9]{8}$/
|
|
static let mobileNumberWithExtra0 = /^33[0][6-7][0-9]{8}$/
|
|
}
|
|
|
|
private func cleanedNumberForValidation() -> String {
|
|
// Keep leading '+' if present, remove all other non-digit characters
|
|
var cleaned = self.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
if cleaned.hasPrefix("+") {
|
|
// Preserve '+' at start, remove all other non-digit characters
|
|
let digitsOnly = cleaned.dropFirst().components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
|
|
cleaned = "+" + digitsOnly
|
|
} else {
|
|
// Remove all non-digit characters
|
|
cleaned = cleaned.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
|
|
}
|
|
return cleaned
|
|
}
|
|
|
|
// MARK: - Phone Number Validation
|
|
|
|
/// Validate if the string is a mobile number for the specified locale.
|
|
/// - Parameter locale: The locale to validate against. Defaults to `.current`.
|
|
/// - Returns: True if the string matches the mobile number pattern for the locale.
|
|
func isMobileNumber(locale: Locale = .current) -> Bool {
|
|
// TODO: Support additional regions/locales in the future.
|
|
switch locale.region?.identifier {
|
|
case "FR", "fr", nil:
|
|
// French logic for now
|
|
let cleaned = cleanedNumberForValidation()
|
|
if cleaned.firstMatch(of: RegexStatic.mobileNumber) != nil {
|
|
return true
|
|
}
|
|
if cleaned.firstMatch(of: RegexStatic.mobileNumberWithExtra0) != nil {
|
|
return true
|
|
}
|
|
return false
|
|
default:
|
|
// For unsupported locales, fallback to checking if the string contains at least 8 digits
|
|
// This is a generic minimum length for most countries' phone numbers
|
|
let digitsOnly = self.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
|
|
return digitsOnly.count >= 8
|
|
}
|
|
}
|
|
|
|
/// Validate if the string is a phone number for the specified locale.
|
|
/// - Parameter locale: The locale to validate against. Defaults to `.current`.
|
|
/// - Returns: True if the string matches the phone number pattern for the locale.
|
|
func isPhoneNumber(locale: Locale = .current) -> Bool {
|
|
// TODO: Support additional regions/locales in the future.
|
|
switch locale.region?.identifier {
|
|
case "FR", "fr", nil:
|
|
// French logic for now
|
|
let cleaned = cleanedNumberForValidation()
|
|
if cleaned.firstMatch(of: RegexStatic.phoneNumber) != nil {
|
|
return true
|
|
}
|
|
if cleaned.firstMatch(of: RegexStatic.phoneNumberWithExtra0) != nil {
|
|
return true
|
|
}
|
|
return false
|
|
default:
|
|
// For unsupported locales, fallback to checking if the string contains at least 8 digits
|
|
// This is a generic minimum length for most countries' phone numbers
|
|
let digitsOnly = self.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
|
|
return digitsOnly.count >= 8
|
|
}
|
|
}
|
|
|
|
func normalize(_ phone: String) -> String {
|
|
var normalized = phone.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
// Remove all non-digit characters
|
|
normalized = normalized.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
|
|
// Remove leading country code for France (33) if present
|
|
if normalized.hasPrefix("33") {
|
|
if normalized.dropFirst(2).hasPrefix("0") {
|
|
// Keep as is, don't strip the zero after 33
|
|
} else {
|
|
normalized = "0" + normalized.dropFirst(2)
|
|
}
|
|
} else if normalized.hasPrefix("0033") {
|
|
normalized = "0" + normalized.dropFirst(4)
|
|
}
|
|
return normalized
|
|
}
|
|
|
|
func isSamePhoneNumber(as other: String) -> Bool {
|
|
return normalize(self) == normalize(other)
|
|
}
|
|
|
|
func cleanSearchText() -> String {
|
|
// Create a character set of all punctuation except slashes and hyphens
|
|
var punctuationToRemove = CharacterSet.punctuationCharacters
|
|
punctuationToRemove.remove(charactersIn: "/-")
|
|
|
|
// Remove the unwanted punctuation
|
|
return self.components(separatedBy: punctuationToRemove)
|
|
.joined(separator: " ")
|
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
}
|
|
|
|
//april 04-2024 bug with accent characters / adobe / fft
|
|
mutating func replace(characters: [(Character, Character)]) {
|
|
for (targetChar, replacementChar) in characters {
|
|
self = String(self.map { $0 == targetChar ? replacementChar : $0 })
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Player Names
|
|
public extension StringProtocol {
|
|
var firstUppercased: String { prefix(1).uppercased() + dropFirst() }
|
|
var firstCapitalized: String { prefix(1).capitalized + dropFirst() }
|
|
}
|
|
|
|
// MARK: - todo clean up ??
|
|
public extension LosslessStringConvertible {
|
|
var string: String { .init(self) }
|
|
}
|
|
|
|
public extension String {
|
|
func createFile(_ withName: String = "temp", _ exportedFormat: ExportFormat = .rawText) -> URL {
|
|
let url = FileManager.default.temporaryDirectory
|
|
.appendingPathComponent(withName)
|
|
.appendingPathExtension(exportedFormat.suffix)
|
|
let string = self
|
|
try? FileManager.default.removeItem(at: url)
|
|
try? string.write(to: url, atomically: true, encoding: .utf8)
|
|
return url
|
|
}
|
|
}
|
|
|
|
public extension String {
|
|
func toInt() -> Int? {
|
|
Int(self)
|
|
}
|
|
}
|
|
|
|
extension String : @retroactive Identifiable {
|
|
public var id: String { self }
|
|
}
|
|
|
|
public extension String {
|
|
/// Parses the birthdate string into a `Date` based on multiple formats.
|
|
/// - Returns: A `Date` object if parsing is successful, or `nil` if the format is unrecognized.
|
|
func parseAsBirthdate() -> Date? {
|
|
let dateFormats = [
|
|
"yyyy-MM-dd", // Format for "1993-01-31"
|
|
"dd/MM/yyyy", // Format for "27/07/1992"
|
|
"dd/MM/yy" // Format for "27/07/92"
|
|
]
|
|
|
|
let dateFormatter = DateFormatter()
|
|
dateFormatter.locale = Locale(identifier: "en_US_POSIX") // Ensure consistent parsing
|
|
|
|
for format in dateFormats {
|
|
dateFormatter.dateFormat = format
|
|
|
|
if let date = dateFormatter.date(from: self) {
|
|
return date // Return the parsed date if successful
|
|
}
|
|
}
|
|
|
|
return nil // Return nil if no format matches
|
|
}
|
|
|
|
/// Formats the birthdate string into "DD/MM/YYYY".
|
|
/// - Returns: A formatted birthdate string, or the original string if parsing fails.
|
|
func formattedAsBirthdate() -> String {
|
|
if let parsedDate = self.parseAsBirthdate() {
|
|
let outputFormatter = DateFormatter()
|
|
outputFormatter.dateFormat = "dd/MM/yyyy" // Desired output format
|
|
return outputFormatter.string(from: parsedDate)
|
|
}
|
|
return self // Return the original string if parsing fails
|
|
}
|
|
}
|
|
|
|
|