Files
LCEssentials/Documentation/Extensions.md

799 lines
22 KiB
Markdown
Raw Normal View History

# LCEssentials — Extensions
Foundation, value-type, string, collection, numeric, date, and crypto helpers, plus
the `LCEssentials` namespace itself. UIKit extensions live in
[UIKit.md](UIKit.md); SwiftUI helpers in [SwiftUI.md](SwiftUI.md).
Every section is a collapsible block — click a heading to expand it.
## Contents
- [API & Networking](#api--networking)
- [Strings & Text](#strings--text)
- [Collections & Sequences](#collections--sequences)
- [Numbers & Geometry](#numbers--geometry)
- [Date, Data & Files](#date-data--files)
- [Encoding & Errors](#encoding--errors)
- [Crypto](#crypto)
- [Core — the `LCEssentials` namespace](#core--the-lcessentials-namespace)
---
## API & Networking
`API` is an `actor` wrapping `URLSession` for typed JSON requests and multipart
uploads. This is a summary — the **[full guide is in API.md](API.md)** (all
parameters, error model, client certificates, custom body types, and the
rationale vs. a hand-rolled `URLSession`).
<details>
<summary><b>API</b> — typed async requests &amp; uploads</summary>
### `API.shared`
The shared `actor` instance. Every call is `await`; nothing runs on the main
thread unless you hop there yourself.
```swift
struct User: Decodable, Sendable { let id: Int; let name: String }
let user: User = try await API.shared.request(
url: "https://api.example.com/users/{id}",
method: .get,
pathParams: ["id": "42"]
)
```
### `request(url:method:body:pathParams:headers:debug:timeoutInterval:networkServiceType:persistConnection:)`
Sends a request and decodes the JSON response into `T: Decodable & Sendable`.
Non-2xx responses throw an `NSError` whose `code` is the HTTP status and whose
`localizedFailureReason` is the response body. When `T == String` the raw body
is returned without JSON decoding.
```swift
struct CreateUser: Encodable, Sendable { let name: String }
// JSON body
let created: User = try await API.shared.request(
url: "https://api.example.com/users",
method: .post,
body: jsonBody(CreateUser(name: "Ana"))
)
// form-url-encoded body
let token: Token = try await API.shared.request(
url: "https://api.example.com/oauth/token",
method: .post,
body: .form(["grant_type": "password", "username": "ana"])
)
```
### `upload(url:method:form:pathParams:headers:debug:timeoutInterval:networkServiceType:)` and its `onProgress:` overload
Uploads a `multipart/form-data` body serialised to a temp file (removed
afterwards) and streamed from disk, so large files never fully load into memory.
```swift
var form = MultipartForm()
form.field("caption", "Sunset")
form.file("photo", data: jpegData, filename: "p.jpg")
form.file("video", url: localVideoURL) // streamed from disk
let result: UploadResult = try await API.shared.upload(
url: "https://api.example.com/media",
form: form,
onProgress: { fraction in
Task { @MainActor in progressView.progress = Float(fraction) }
}
)
```
### Body types — `HTTPBody`
`jsonBody(_:)` wraps any `Encodable & Sendable`; `FormURLEncodedBody` (a.k.a.
`.form([:])`) percent-escapes every value and never drops one; `RawBody` lets you
supply the bytes and `Content-Type` yourself. Conform your own type to `HTTPBody`
and `API` accepts it with no change.
```swift
struct CSVBody: HTTPBody {
let rows: [[String]]
func encoded() throws -> (data: Data, contentType: String) {
let text = rows.map { $0.joined(separator: ",") }.joined(separator: "\n")
return (Data(text.utf8), "text/csv; charset=UTF-8")
}
}
```
### `await API.shared.setupCertification(certData:password:)`
Registers a client certificate (`.p12`) for mutual-TLS on subsequent requests.
```swift
let p12 = try Data(contentsOf: certURL)
await API.shared.setupCertification(certData: p12, password: "cert-pw")
```
</details>
## Strings & Text
<details>
<summary><b>String</b> — validation, parsing, formatting, masks, HTML, dates</summary>
### URLs
#### `var isValidUrl: Bool` / `var isValidHttpsUrl: Bool` / `var isValidHttpUrl: Bool`
`isValidUrl` is true when `URL(string:)` succeeds; the other two additionally
require the `https` / `http` scheme.
```swift
"https://google.com".isValidUrl // true
"https://google.com".isValidHttpsUrl // true
"http://google.com".isValidHttpsUrl // false
```
#### `var urlEncoded: String` / `var urlDecoded: String`
Percent-encode (host-allowed set) / decode. `urlDecoded` returns the original
string when it is not encoded.
```swift
"it's easy".urlEncoded // "it's%20easy"
"it's%20easy".urlDecoded // "it's easy"
```
#### `mutating func urlEncode() -> String` / `mutating func urlDecode() -> String`
In-place variants; also return the new value (`@discardableResult`).
```swift
var s = "a b"; s.urlEncode() // s == "a%20b"
```
#### `func stringByAddingPercentEncodingForRFC3986() -> String`
Percent-encodes for use as a query key/value, escaping `;/?:@&=+$,` and space.
```swift
"a&b=c".stringByAddingPercentEncodingForRFC3986() // "a%26b%3Dc"
```
#### `var url: String?`
First URL detected **inside** the string (via `NSDataDetector`), or `nil`.
```swift
"visit www.site.com.br now".url // "www.site.com.br"
```
#### `var toURL: NSURL?`
`NSURL(string:)` wrapper.
```swift
"https://x.com".toURL // NSURL
```
### Validation
#### `var isEmail: Bool`
Regex check for a syntactically valid email (TLD 220 chars).
```swift
"user@example.com".isEmail // true
"nope".isEmail // false
```
#### `var isCPF: Bool`
Validates a Brazilian CPF including both check digits. Strips formatting first
(`onlyNumbers`), requires 11 digits.
```swift
"123.456.789-09".isCPF // true when the check digits match
```
#### `var isValidCNPJ: Bool`
Validates a Brazilian CNPJ (14 digits, both check digits, rejects all-same-digit).
```swift
"11.222.333/0001-81".isValidCNPJ // true when valid
```
#### `var isAlphabetic: Bool`
Letters only, no digits.
```swift
"abc".isAlphabetic // true
"123abc".isAlphabetic // false
```
#### `var isAlphaNumeric: Bool`
Contains at least one letter **and** one digit and nothing else — handy for
password rules.
```swift
"123abc".isAlphaNumeric // true
"abc".isAlphaNumeric // false
```
#### `var isHTML: Bool`
True if the string contains an HTML tag.
```swift
"<b>hi</b>".isHTML // true
```
#### `func validateBolean(comparingBoolean: Bool = true) -> Bool`
Loose truthy/falsy check against a large set of EN/PT words. With
`comparingBoolean: true` returns whether the string means "true"
(`YES`, `ON`, `SIM`, `ATIVO`, `1`, `T`, …); with `false`, whether it means "false".
```swift
"SIM".validateBolean() // true
"nao".validateBolean(comparingBoolean: false) // true
```
### Conversion
#### `var bool: Bool?`
`"true"/"yes"/"1"``true`, `"false"/"no"/"0"``false`, else `nil` (trimmed, case-insensitive).
```swift
" YES ".bool // true
"maybe".bool // nil
```
#### `var int: Int?` / `var float: Float?` / `var double: Double?`
Plain `Int(self)` / `Float(self)` / `Double(self)`.
```swift
"101".int // 101
"1.5".double // 1.5
"x".int // nil
```
#### `func float(locale: Locale = .current) -> Float?` / `func double(locale:) -> Double?`
Locale-aware parsing via `NumberFormatter` (accepts grouping separators).
```swift
"1,5".double(locale: Locale(identifier: "pt_BR")) // 1.5
```
#### `var currencyStringToDouble: Double`
Parses a `pt_BR` currency string to `Double`, `0.0` on failure.
```swift
"R$ 1.234,56".currencyStringToDouble // 1234.56
```
#### `var btcToSats: Int` / `var bitcoinToSatoshis: Int`
Multiplies a BTC amount string by 100,000,000. `bitcoinToSatoshis` is an alias.
```swift
"0.0001".btcToSats // 10000
```
#### `var data: Data`
UTF-8 bytes.
```swift
"hi".data // 2 bytes
```
#### `var nsString: NSString` / `var fullNSRange: NSRange`
Bridge to `NSString`; `NSRange` spanning the whole string (UTF-16 aware).
```swift
"café".fullNSRange // {0, 4}
```
#### `func nsRange(from range: Range<String.Index>) -> NSRange?`
Convert a Swift `Range` to an `NSRange` in the UTF-16 view.
#### `var base64Encode: String?` / `var base64Decode: String?`
Base64 encode the UTF-8 bytes / decode a Base64 string back to text.
```swift
"hi".base64Encode // "aGk="
"aGk=".base64Decode // "hi"
```
#### `func date(withCurrFormatt:localeIdentifier:timeZone:) -> Date?`
Parse the string to `Date` using the given input format (default
`"yyyy-MM-dd HH:mm:ss"`, locale `pt-BR`, current time zone). A `" 0000"` suffix
is normalised to `" +0000"`.
```swift
"2026-08-29 14:30:00".date() // Date
```
#### `func date(withCurrFormatt:newFormatt:localeIdentifier:timeZone:) -> Date?`
Parse with one format and round-trip through another (normalises the value).
#### `var currentTimeZone: String`
Current time-zone offset string, e.g. `"-0300"`.
### Cleaning & filtering
#### `var withoutSpacesAndNewLines: String`
Removes every space and `\n`.
```swift
" a \n b ".withoutSpacesAndNewLines // "ab"
```
#### `var onlyNumbers: String` / `var numbers: String`
Digits only. `onlyNumbers` uses a `\D` regex; `numbers` uses `decimalDigits`.
```swift
"(11) 98765-4321".onlyNumbers // "11987654321"
```
#### `var letters: String` / `var lettersWithWhiteSpace: String`
Keep only letters (optionally keeping spaces).
```swift
"a1 b2".letters // "ab"
"a1 b2".lettersWithWhiteSpace // "a b"
```
#### `var alphanumeric: String` / `var alphanumericWithWhiteSpace: String`
Keep only alphanumerics (optionally keeping spaces).
```swift
"a-b_c 1".alphanumeric // "abc1"
```
#### `var removeSpecialChars: String`
Keeps `[A-Za-z0-9 -]` only.
```swift
"a@b#c".removeSpecialChars // "abc"
```
#### `var removeHTMLTags: String` / `var removeEmoji: String`
Strip HTML tags / strip emoji (`CharacterSet.symbols`).
```swift
"<p>hi</p>".removeHTMLTags // "hi"
"hi 😀".removeEmoji // "hi "
```
### Slicing & padding
#### `var first: String` / `var last: String`
First / last character **as a String** (`""` when empty).
#### `var uppercaseFirst: String`
Capitalises the first character only.
```swift
"hello".uppercaseFirst // "Hello"
```
#### `var firstCharacterAsString: String?` / `var lastCharacterAsString: String?`
Optional variants — `nil` when empty.
#### `func paddingStart(_ length: Int, with: String = " ") -> String` / `func paddingEnd(...)`
Pad to `length` with a repeating pad string at the start / end. No-op if already long enough.
```swift
"hue".paddingStart(10) // " hue"
"hue".paddingEnd(10, with: "br") // "huebrbrbrb"
```
#### `func truncated(toLength: Int, trailing: String? = "...") -> String`
Non-mutating truncation.
```swift
"This is long".truncated(toLength: 7) // "This is..."
```
#### `mutating func truncate(toLength: Int, trailing: String? = "...") -> String`
In-place truncation (`@discardableResult`).
#### `mutating func trim() -> String`
Trim leading/trailing whitespace and newlines, in place (`@discardableResult`).
```swift
var s = " hi \n"; s.trim() // s == "hi"
```
#### `mutating func reverse() -> String`
Reverse in place (`@discardableResult`).
#### `mutating func insertAtIndexEnd(string:ind:)` / `insertAtIndexStart(string:ind:)`
Insert `string` at an offset measured from `endIndex` (negative `ind` moves left).
```swift
var s = "abcd"; s.insertAtIndexEnd(string: "-", ind: -1) // "abc-d"
```
### Replacing
#### `func replace(from:to:)` / `func findAndReplace(from:to:)`
Simple substring replacement (`findAndReplace` is generic over `StringProtocol`).
```swift
"a.b.c".replace(from: ".", to: "-") // "a-b-c"
```
#### `func replacing(range: CountableClosedRange<Int>, with: String) -> String`
Replace by integer character range.
```swift
"abcdef".replacing(range: 1...3, with: "X") // "aXef"
```
#### `func replacingLastOccurrenceOfString(_:with:caseInsensitive: Bool = true) -> String`
Replace only the last match.
```swift
"a-b-c".replacingLastOccurrenceOfString("-", with: "+") // "a-b+c"
```
#### `func replaceAll(of pattern: String, with: String, options: = []) -> String`
Regex replace-all; returns the original on a bad pattern.
```swift
"a1b2c3".replaceAll(of: "[0-9]", with: "#") // "a#b#c#"
```
#### `@discardableResult func replaceURL(_ withDict: [String: Any]) -> String`
Substitute `{key}` placeholders — used by `API.request(pathParams:)`.
```swift
"/users/{id}/posts/{p}".replaceURL(["id": 7, "p": "x"]) // "/users/7/posts/x"
```
### Words & search
#### `func words() -> [String]` / `func wordCount() -> Int`
Split on whitespace + punctuation, dropping empties.
```swift
"Swift is amazing".words() // ["Swift", "is", "amazing"]
"Swift is amazing".wordCount() // 3
```
#### `func contains(_:caseSensitive: Bool = true) -> Bool`
Substring check with optional case-insensitivity.
```swift
"Hello".contains("ell") // true
"Hello".contains("HELLO", caseSensitive: false) // true
```
### Formatting helpers
#### `func applyMask(toText: String, mask: String) -> String`
Apply a `#`-placeholder mask; literal characters in the mask are inserted.
```swift
"11987654321".applyMask(toText: "11987654321", mask: "(##) #####-####")
// "(11) 98765-4321"
```
#### `func exponentize(str: String) -> String`
Turn `^`-prefixed digits into Unicode superscripts.
```swift
"x^2 + y^3".exponentize(str: "x^2 + y^3") // "x² + y³"
```
#### `func stringFromTimeInterval(_ interval: TimeInterval) -> NSString`
Format a `TimeInterval` as `HH:MM:SS.mmm`.
```swift
"".stringFromTimeInterval(3661.5) // "01:01:01.500"
```
#### `func toSlug() -> String`
Lowercase, de-accent, spaces → `-`, strip other punctuation.
```swift
"Olá Mundo!".toSlug() // "ola-mundo"
```
#### `func localized(comment: String = "") -> String`
`NSLocalizedString(self, comment:)`.
```swift
"welcome_title".localized()
```
### Generators & misc
#### `static func loremIpsum(ofLength length: Int = 445) -> String`
Lorem-ipsum text truncated to `length` (max 445).
```swift
String.loremIpsum(ofLength: 20) // "Lorem ipsum dolor si"
```
#### `func randomString(length: Int) -> String`
Random `[A-Za-z0-9]` string. (Instance method — the receiver is ignored.)
```swift
"".randomString(length: 8) // e.g. "a9Fk2Lp0"
```
#### `var JSONStringToDictionary: [String: Any]?`
Parse a JSON object string to a dictionary (`nil` + logs on failure).
```swift
#"{"a":1}"#.JSONStringToDictionary // ["a": 1]
```
#### `var convertToHTML: NSAttributedString?`
Render an HTML string to `NSAttributedString` (UIKit path uses the CSS converter below).
#### `func convertHtmlToAttributedStringWithCSS(font:csscolor:lineheight:csstextalign:customCSS:) -> NSAttributedString?` — *UIKit only*
HTML → `NSAttributedString` with an injected `<style>` block. Returns the plain
HTML rendering when `font` is `nil`.
```swift
"<b>Hi</b>".convertHtmlToAttributedStringWithCSS(
font: .systemFont(ofSize: 16), csscolor: "#333",
lineheight: 0, csstextalign: "left"
)
```
#### `func height(withConstrainedWidth:font:) -> CGFloat` / `func width(withConstraintedHeight:font:) -> CGFloat` — *UIKit only*
Measured bounding height/width for the string at a fixed width/height and font.
```swift
"Some label text".height(withConstrainedWidth: 200, font: .systemFont(ofSize: 14))
```
</details>
<details>
<summary><b>Character</b> — classification, conversion, repetition</summary>
### `var isEmoji: Bool`
True when the scalar falls in a known emoji range.
```swift
Character("😀").isEmoji // true
Character("a").isEmoji // false
```
### `var int: Int?` / `var string: String`
Digit value (or `nil`) / one-character `String`.
```swift
Character("7").int // 7
Character("A").int // nil
```
### `var lowercased: Character` / `var uppercased: Character`
Case-flipped character.
```swift
Character("a").uppercased // "A"
```
### `func unicodeScalarCodePoint() -> UInt32`
First Unicode scalar value.
```swift
Character("A").unicodeScalarCodePoint() // 65
```
### `static func randomAlphanumeric() -> Character`
Random `[A-Za-z0-9]` character.
```swift
Character.randomAlphanumeric() // e.g. "k"
```
### `static func * (Character, Int) -> String` / `static func * (Int, Character) -> String`
Repeat a character into a string.
```swift
Character("-") * 5 // "-----"
5 * Character("-") // "-----"
```
</details>
<details>
<summary><b>NSString</b></summary>
### `var string: String?`
`String(describing:)` of the `NSString`.
### `func randomAlphaNumericString(_ length: Int = 8) -> String`
Random `[A-Za-z0-9]` string of the given length.
```swift
("" as NSString).randomAlphaNumericString(12)
```
</details>
<details>
<summary><b>NSAttributedString / NSMutableAttributedString</b></summary>
### `NSAttributedString(html: String)` — *failable init*
Build an attributed string from an HTML fragment.
```swift
label.attributedText = NSAttributedString(html: "<b>Hello</b> world")
```
### `NSMutableAttributedString` builders — *UIKit only*, all `@discardableResult` and chainable
| Method | Effect |
|---|---|
| `customize(_:withFont:color:lineSpace:alignment:changeCurrentText:)` | append (or restyle) a run with font/color/spacing/alignment |
| `underline(_:withFont:color:changeCurrentText:)` | underlined run |
| `strikethrough(_:changeCurrentText:)` | strikethrough run |
| `linkTouch(_:url:withFont:color:changeCurrentText:)` | tappable link run |
| `supperscript(_:withFont:color:offset:changeCurrentText:)` | baseline-offset (superscript) run |
| `appendImageToText(_:)` | append an inline `UIImage` attachment |
| `normal(_:)` | append an unstyled run |
`changeCurrentText: true` restyles the first occurrence of the text already in
the string instead of appending.
```swift
let s = NSMutableAttributedString()
.customize("Total: ", withFont: .systemFont(ofSize: 14))
.customize("R$ 10", withFont: .boldSystemFont(ofSize: 14), color: .label)
.supperscript("00", withFont: .systemFont(ofSize: 9), offset: 6)
label.attributedText = s
```
### `func canSetAsLink(textToFind: String, linkURL: String) -> Bool`
Adds a `.link` attribute to the first match; returns whether it was found.
### `func attributtedString() -> NSAttributedString`
Immutable copy of the whole string.
### `func height(withConstrainedWidth:) -> CGFloat` / `func width(withConstrainedHeight:) -> CGFloat`
Bounding size for the attributed content.
</details>
<details>
<summary><b>Bytes</b> — <code>Sequence where Element == UInt8</code></summary>
### `var data: Data`
Wrap the byte sequence in `Data`.
### `var base64Decoded: Data?`
Interpret the bytes as Base64 text and decode.
### `var string: String?`
Decode the bytes as UTF-8.
```swift
let bytes: [UInt8] = [0x68, 0x69]
bytes.data // 2 bytes
bytes.string // "hi"
```
</details>
## Collections & Sequences
<!-- batch 4 -->
## Numbers & Geometry
<!-- batch 5 -->
## Date, Data & Files
<!-- batch 6 -->
## Encoding & Errors
<!-- batch 7 -->
## Crypto
<details>
<summary><b>RIPEMD_160</b> — RIPEMD-160 digest</summary>
### `static func hash(_ message: Data) -> Data`
Returns the 20-byte RIPEMD-160 digest of `message`. Pure Swift, no system
dependency. Mainly useful for Bitcoin-style address hashing (`RIPEMD160(SHA256(x))`).
```swift
let digest = RIPEMD_160.hash(Data("abc".utf8))
digest.count // 20
digest.map { String(format: "%02x", $0) }.joined()
// "8eb208f7e05d987a9b044a8e98c6b087f15a0bfc"
```
</details>
<details>
<summary><b>LCECryptoKitManager</b> — OTP / peppered-login bridge (needs the <code>LCECryptoKit</code> binary)</summary>
A thin facade over the optional `LCECryptoKit` binary product (enabled by the
`LCE_ENABLE_CRYPTO_BINARY` build flag). **When the binary is not linked every
method is a no-op** returning `nil` / `""` / `false`, so calling code still
compiles and runs.
### `init()` / `init(privateKey:)`
Create a manager. The `privateKey` (a.k.a. "hash key") is only needed by the
`*WithKey` methods.
```swift
let crypto = LCECryptoKitManager()
let keyed = LCECryptoKitManager(privateKey: serverHashKey)
```
### `static func generateKey() -> String`
Generates a random AES key string.
```swift
let key = LCECryptoKitManager.generateKey()
```
### `func encodeTP(email:password:) -> String?` / `func decodeOTP(_:) -> String?`
Encode an email+password pair into an OTP seed hash, and decode it back.
```swift
let hash = crypto.encodeTP(email: "ana@x.com", password: "s3cr3t")
let back = crypto.decodeOTP(hash ?? "")
```
### `func encodeOTPWithKey(email:password:) -> String?` / `func decodeOTPWithKey(_:) -> Bool`
Same as above but bound to the instance's `privateKey`; `decodeOTPWithKey`
returns whether the hash validates against that key rather than the decoded value.
```swift
let keyed = LCECryptoKitManager(privateKey: serverHashKey)
let hash = keyed.encodeOTPWithKey(email: "ana@x.com", password: "s3cr3t")
let ok = keyed.decodeOTPWithKey(hash ?? "") // Bool
```
### `static func generateSalt() -> String`
Random salt for the salted/iterated/peppered login flow.
```swift
let salt = LCECryptoKitManager.generateSalt()
```
### `static func computeClientHash(email:password:salt:) -> String`
Client-side hash of the credentials with the given salt — sent to the server
instead of the raw password.
```swift
let clientHash = LCECryptoKitManager.computeClientHash(
email: "ana@x.com", password: "s3cr3t", salt: salt
)
```
### `static func computeLoginBearerToken(userId:clientHash:) -> String?`
Derives the login bearer token from a user id and the client hash.
```swift
let token = LCECryptoKitManager.computeLoginBearerToken(userId: "42", clientHash: clientHash)
```
### `static func otpEncode(_:) -> String?` / `static func otpDecode(_:) -> String?`
One-time-pad encode/decode of an arbitrary string.
```swift
let enc = LCECryptoKitManager.otpEncode("secret-value")
let dec = LCECryptoKitManager.otpDecode(enc ?? "") // "secret-value"
```
</details>
## Core — the `LCEssentials` namespace
<!-- batch 7 -->