Skip to content

Commit

Permalink
Add a bunch of extension and utilities from our main project. (#34)
Browse files Browse the repository at this point in the history
* Add a bunch of extension and utilities from our main project.

* Visibility

* Improve testing

* Fix tests

* Coverage up!

* Coverage bump again

* Bump tests
  • Loading branch information
sebastianvarela authored Feb 1, 2024
1 parent 2a974bd commit 571ac4b
Show file tree
Hide file tree
Showing 44 changed files with 1,217 additions and 322 deletions.
202 changes: 202 additions & 0 deletions MasMagicPills.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions Source/Combine/Extensions/CurrentValueSubjectExtensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Combine
import Foundation

public extension CurrentValueSubject where Output: Equatable {
/// Emit new value if distinct, ignore
func sendIfDistinct(_ input: Output) {
if input != value {
send(input)
}
}
}
12 changes: 12 additions & 0 deletions Source/Combine/Extensions/PassthroughSubjectExtensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Combine
import Foundation

public extension PassthroughSubject where Output == String {
func sendType<T>(_ type: T.Type, tag: String = "") {
send("\(type.self)-\(tag)")
}

func filterType<T>(_ type: T.Type, tag: String = "") -> Publishers.Filter<PassthroughSubject<Output, Failure>> {
filter { $0 == "\(type.self)-\(tag)" }
}
}
15 changes: 15 additions & 0 deletions Source/Combine/Extensions/PublisherExtensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Combine
import Foundation

public extension Publisher {
/**
Projects each element from a publisher into a new publisher and then transforms an publisher into an publisher producing values only from the most recent value.
- parameter transform: A transform function to apply to each element.
- returns: A publisher whose elements are the result of invoking the transform function on each value of source producing a publisher and that at any point in time produces the elements of the most recent inner sequence that has been received.
*/
func flatMapLatest<T>(_ transform: @escaping (Output) -> AnyPublisher<T, Failure>) -> AnyPublisher<T, Failure> {
map(transform)
.switchToLatest()
.eraseToAnyPublisher()
}
}
84 changes: 84 additions & 0 deletions Source/Combine/Publishers/Publishers.WithLatestFrom.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import Combine
import Foundation

public extension Publisher {
/// Emit with the latest value from given publisher, if the given publisher is empty upstream value will loss.
func withLatestFrom<Other: Publisher>(_ other: Other) -> Publishers.WithLatestFrom<Self, Other> {
Publishers.WithLatestFrom(upstream: self, other: other)
}
}

public extension Publishers {
struct WithLatestFrom<Upstream: Publisher, Other: Publisher>: Publisher where Upstream.Failure == Other.Failure {
public typealias Output = (Upstream.Output, Other.Output)
public typealias Failure = Upstream.Failure

public let upstream: Upstream
public let other: Other

public init(upstream: Upstream, other: Other) {
self.upstream = upstream
self.other = other
}

public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Output == Downstream.Input, Downstream.Failure == Upstream.Failure {
let merged = mergedStream(upstream, other)
let result = resultStream(from: merged)
result.subscribe(subscriber)
}
}
}

private extension Publishers.WithLatestFrom {
// MARK: - Types
enum MergedElement {
case upstream1(Upstream.Output)
case upstream2(Other.Output)
}

typealias ScanResult =
(value1: Upstream.Output?,
value2: Other.Output?, shouldEmit: Bool)

// MARK: - Pipelines
func mergedStream(_ upstream1: Upstream, _ upstream2: Other)
-> AnyPublisher<MergedElement, Failure> {
let mergedElementUpstream1 = upstream1
.map { MergedElement.upstream1($0) }
let mergedElementUpstream2 = upstream2
.map { MergedElement.upstream2($0) }
return mergedElementUpstream1
.merge(with: mergedElementUpstream2)
.eraseToAnyPublisher()
}

func resultStream(
from mergedStream: AnyPublisher<MergedElement, Failure>
) -> AnyPublisher<Output, Failure> {
mergedStream
.scan(nil) { (prevResult: ScanResult?, mergedElement: MergedElement) -> ScanResult? in
var newValue1: Upstream.Output?
var newValue2: Other.Output?
let shouldEmit: Bool

switch mergedElement {
case .upstream1(let value):
newValue1 = value
shouldEmit = prevResult?.value2 != nil

case .upstream2(let value):
newValue2 = value
shouldEmit = false
}

return ScanResult(value1: newValue1 ?? prevResult?.value1,
value2: newValue2 ?? prevResult?.value2,
shouldEmit: shouldEmit)
}
.compactMap { $0 }
.filter { $0.shouldEmit }
.map { Output($0.value1!, $0.value2!) }
.eraseToAnyPublisher()
}
}
41 changes: 30 additions & 11 deletions Source/Foundation/Extensions/BundleExtensions.swift
Original file line number Diff line number Diff line change
@@ -1,23 +1,42 @@
import Foundation

public extension Bundle {
/// The release ("Major"."Minor"."Patch") or version number of the bundle (read-only, optional)
var versionNumber: String? {
object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
/// The release ("Major"."Minor"."Patch") or version number of the bundle
var versionNumber: String {
(object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String) ?? "0"
}

@available(*, deprecated, renamed: "versionNumber")
var appVersion: String {
versionNumber
}

/// The version number of the bundle. (read-only, optional)
var buildNumber: String? {
object(forInfoDictionaryKey: kCFBundleVersionKey as String) as? String
var buildNumber: String {
(object(forInfoDictionaryKey: kCFBundleVersionKey as String) as? String) ?? "0"
}

@available(*, deprecated, renamed: "buildNumber")
var appBuild: String {
buildNumber
}

/// Release number with version number or version number if are the same (read-only, optional)
var fullVersionNumber: String? {
guard let versionNumber = self.versionNumber,
let buildNumber = self.buildNumber else {
return nil
}
var fullVersionNumber: String {
versionNumber == buildNumber ? "v\(versionNumber)" : "v\(versionNumber)(\(buildNumber))"
}

@available(*, deprecated, renamed: "fullVersionNumber")
var appFullVersion: String {
fullVersionNumber
}

var isRunningFromTestFlight: Bool {
appStoreReceiptURL?.lastPathComponent == "sandboxReceipt"
}

return versionNumber == buildNumber ? "v\(versionNumber)" : "v\(versionNumber)(\(buildNumber))"
@available(*, deprecated, renamed: "isRunningFromTestFlight")
var runningFromTestFlight: Bool {
isRunningFromTestFlight
}
}
26 changes: 26 additions & 0 deletions Source/Foundation/Extensions/DecimalExtensions.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,32 @@
import Foundation

public extension Decimal {
/// Return the given percent from current value (self)
/// - Returns: Decimal
func asPercentage(_ value: Decimal) -> Decimal {
self / (100 / value)
}

/// Return the negated value
var negated: Decimal {
-self
}

/// Return true if value is != 0
var isNotZero: Bool {
!isZero
}

/// Return double from decimal value.
var doubleValue: Double {
Double(truncating: self as NSNumber)
}

@available(*, deprecated, renamed: "doubleValue")
var asDouble: Double {
doubleValue
}

/// Convert milliseconds(self) to seconds
var millisecondsToSeconds: Decimal {
self / 1_000
Expand Down
11 changes: 10 additions & 1 deletion Source/Foundation/Extensions/IntExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,17 @@ public extension Int {
"\(self)"
}

var decimalValue: Decimal {
Decimal(self)
}

@available(*, deprecated, renamed: "decimalValue")
var toDecimal: Decimal {
decimalValue
}

@available(*, deprecated, renamed: "stringValue")
var toString: String {
"\(self)"
stringValue
}
}
34 changes: 34 additions & 0 deletions Source/Foundation/Extensions/StringExtensions+Crypto.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,40 @@ public extension String {
return hash.map { String(format: "%02x", $0) }.joined().uppercased()
}

/// Decode Base64 string if possible. Returns nil if fails.
var base64decoded: String? {
guard let data = Data(base64Encoded: self, options: .ignoreUnknownCharacters) else {
return nil
}

return String(data: data, encoding: .utf8)
}

/// Decode Base64 URL-Safe string if possible. Returns nil if fails.
var base64UrlDecoded: String? {
var base64 = self
.replacingOccurrences(of: "_", with: "/")
.replacingOccurrences(of: "-", with: "+")

if base64.count % 4 != 0 {
base64.append(String(repeating: "=", count: 4 - base64.count % 4))
}
return base64.base64decoded
}

/// Encode into Base64 String
var base64encoded: String {
dataUTF8.base64EncodedString()
}

/// Encode into Base64 URL-Safe string.
var base64UrlEncoded: String {
base64encoded
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "=", with: "")
}

func hmac(_ digest: HMACDigest) -> String {
switch digest {
case .sha256(let secret):
Expand Down
50 changes: 50 additions & 0 deletions Source/Foundation/Extensions/StringExtensions+Formating.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,54 @@ public extension String {
}
return formatted
}

/// Return numbers from string
var onlyNumbers: String {
self.replacingOccurrences(of: "[^0-9]", with: "", options: .regularExpression)
}

func removing(prefix: String) -> String {
guard hasPrefix(prefix) else { return self }
return String(dropFirst(prefix.count))
}

func removing(suffix: String) -> String {
guard hasSuffix(suffix) else { return self }
return String(dropLast(suffix.count))
}

var addingTrailingSpaceIfNotEmpty: String {
isEmpty ? "" : "\(self) "
}

var capitalizedWords: String {
self.split(separator: " ")
.map { $0.capitalized }
.joined(separator: " ")
}

var capitalizedSentences: String {
self.components(separatedBy: ". ")
.map { String($0).capitalizedFirstLetter }
.joined(separator: ". ")
}

var capitalizedFirstLetter: String {
if starts(withAnyOf: ["¡", "¿"]) {
return prefix(2).uppercased() + dropFirst(2)
}
return prefix(1).uppercased() + dropFirst(1)
}

var lowercasedLeastTheFirstUnchanged: String {
prefix(1) + dropFirst().lowercased()
}

var removingWhiteSpaces: String {
components(separatedBy: .whitespaces).joined()
}

var trimmed: String {
trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
}
}
58 changes: 58 additions & 0 deletions Source/Foundation/Extensions/StringExtensions+Ranges.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import Foundation

public extension String {
/// Look for where is the first text occurence in string
///
/// - Parameters:
/// - text: Text to look for
/// - options: Options to compare
/// - range: Range of string where look for
/// - locale: Information for use in formatting data for presentation
/// - Returns: Range for the first text occurence in string (optional)
func firstRangeOcurrence(_ text: String,
options: String.CompareOptions = [],
range: Range<Index>? = nil,
locale: Locale? = nil) -> NSRange? {
guard let range = self.range(of: text,
options: options,
range: range ?? startIndex..<endIndex,
locale: locale ?? .current) else { return nil }
return NSRange(range, in: self)
}

/// Look Ranges of texts occurrences in all string
///
/// - Parameters:
/// - text: Text to look for
/// - options: Options to compare
/// - range: Range of string where look for
/// - locale: Information for use in formatting data for presentation
/// - Returns: Ranges of texts occurrences in all string (if not find any, return empty array)
func ocurrencesRanges(_ text: String,
options: String.CompareOptions = [],
range: Range<Index>? = nil,
locale: Locale? = nil) -> [NSRange] {
var start = range?.lowerBound ?? startIndex
let end = range?.upperBound ?? endIndex
var ranges: [NSRange] = []
while start < end, let range = self.range(of: text,
options: options,
range: start..<end,
locale: locale ?? .current) {
ranges.append(NSRange(range, in: self))
start = range.upperBound
}
return ranges
}

var bodyRange: NSRange {
nsRange(of: self)!
}

func nsRange(of string: String) -> NSRange? {
guard let range = self.range(of: string, options: [], range: startIndex..<endIndex, locale: .current) else {
return nil
}
return NSRange(range, in: self)
}
}
Loading

0 comments on commit 571ac4b

Please sign in to comment.