Skip to content

Commit

Permalink
Typesafe sorting and filtering (#37)
Browse files Browse the repository at this point in the history
- Typesafe sorting and filtering
- Improved how page bundles are sorted, filtered
  • Loading branch information
viaszkadi authored Oct 17, 2024
1 parent 646e7c5 commit 9b3f8b4
Show file tree
Hide file tree
Showing 8 changed files with 267 additions and 90 deletions.
2 changes: 0 additions & 2 deletions Sources/ToucanSDK/ContentType/ContentType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ struct ContentType: Codable {
case double
case bool
case date
case array
case object
}

let type: DataType
Expand Down
94 changes: 8 additions & 86 deletions Sources/ToucanSDK/ContextStore/ContextStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,85 +8,6 @@
import Foundation
import Logging

// TODO: better sort algorithm using data types
extension [PageBundle] {

func sorted(
key: String?,
order: ContentType.Order?
) -> [PageBundle] {
guard let key, let order else {
return self
}
switch key {
case "publication":
return sorted { lhs, rhs in
switch order {
case .asc:
return lhs.publication < rhs.publication
case .desc:
return lhs.publication > rhs.publication
}
}
default:
return sorted { lhs, rhs in
guard
let l = lhs.frontMatter[key] as? String,
let r = rhs.frontMatter[key] as? String
else {
guard
let l = lhs.frontMatter[key] as? Int,
let r = rhs.frontMatter[key] as? Int
else {
return false
}
switch order {
case .asc:
return l < r
case .desc:
return l > r
}
}
// TODO: proper case insensitive compare
switch order {
case .asc:
// switch l.caseInsensitiveCompare(r) {
// case .orderedAscending:
// return true
// case .orderedDescending:
// return false
// case .orderedSame:
// return false
// }
return l.lowercased() < r.lowercased()
case .desc:
return l.lowercased() > r.lowercased()
}
}
}
}

func limited(_ value: Int?) -> [PageBundle] {
Array(prefix(value ?? Int.max))
}

func filtered(_ filter: ContentType.Filter?) -> [PageBundle] {
guard let filter else {
return self
}
return self.filter { pageBundle in
guard let field = pageBundle.frontMatter[filter.field] else {
return false
}
switch filter.method {
case .equals:
// this is horrible... 😱
return String(describing: field) == filter.value
}
}
}
}

struct ContextStore {

let sourceConfig: SourceConfig
Expand Down Expand Up @@ -202,7 +123,7 @@ struct ContextStore {
.filter { item in
refIds.contains(item.contextAwareIdentifier)
}
.sorted(key: value.sort, order: value.order)
.sorted(frontMatterKey: value.sort, order: value.order)
.limited(value.limit)

result[key] = refs
Expand Down Expand Up @@ -272,7 +193,7 @@ struct ContextStore {
let refs =
pageBundles
.filter { $0.contentType.id == value.references }
.sorted(key: value.sort, order: value.order)
.sorted(frontMatterKey: value.sort, order: value.order)

guard
let idx = refs.firstIndex(where: {
Expand Down Expand Up @@ -321,7 +242,7 @@ struct ContextStore {
)
.contains(id)
}
.sorted(key: value.sort, order: value.order)
.sorted(frontMatterKey: value.sort, order: value.order)
.limited(value.limit)
}
}
Expand Down Expand Up @@ -354,18 +275,19 @@ struct ContextStore {

func getPageBundlesForSiteContext() -> [String: [PageBundle]] {
var result: [String: [PageBundle]] = [:]
let dateFormatter = DateFormatters.baseFormatter

for contentType in contentTypes {
for (key, value) in contentType.context?.site ?? [:] {
result[key] =
pageBundles
.filter { $0.contentType.id == contentType.id }
.sorted(key: value.sort, order: value.order)
.filtered(value.filter)
// TODO: proper pagination
.sorted(frontMatterKey: value.sort, order: value.order)
.filtered(value.filter, dateFormatter: dateFormatter)
.limited(value.limit)
}
}

return result
}

}
73 changes: 73 additions & 0 deletions Sources/ToucanSDK/Extensions/PageBundle+Array.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//
// File.swift
// toucan
//
// Created by Viasz-Kádi Ferenc on 2024. 10. 17..
//

import Foundation
import Yams

extension [PageBundle] {

/// Sorts an array of PageBundle objects based on the given key and order.
///
/// - Parameters:
/// - key: The key used for sorting the PageBundle objects.
/// - order: The order in which the sorting should be done (e.g., ascending or descending).
///
/// - Returns: A sorted array of PageBundle objects, or the original array if key or order is nil.
func sorted(
frontMatterKey: String?,
order: ContentType.Order?
) -> [PageBundle] {
guard
let frontMatterKey,
let order
else {
return self
}
return sorted { lhs, rhs in
lhs.compareForSorting(
for: rhs,
frontMatterKey: frontMatterKey,
order: order
)
}
}

/// Limits the number of elements in the collection to the specified value if provided.
/// If no value is provided, returns the entire collection.
///
/// - Parameters:
/// - value: An optional integer specifying the maximum number of elements to return.
///
/// - Returns: An array containing up to the specified number of elements, or the entire collection if no value is provided.
func limited(_ value: Int?) -> [PageBundle] {
guard let value else {
return self
}
return Array(prefix(value))
}

/// Filters an array of `PageBundle` objects based on the provided filter and date formatter.
///
/// - Parameters:
/// - filter: An optional `ContentType.Filter` to apply. If `nil`, the original array is returned.
/// - dateFormatter: A `DateFormatter` used for date-based filtering.
///
/// - Returns: A filtered array of `PageBundle` objects. If no filter is provided, the original array is returned.
func filtered(
_ filter: ContentType.Filter?,
dateFormatter: DateFormatter
) -> [PageBundle] {
guard let filter else {
return self
}

let result = self.filter {
$0.checkFilter(filter, dateFormatter: dateFormatter)
}
return result
}
}
111 changes: 111 additions & 0 deletions Sources/ToucanSDK/Extensions/PageBundle+Equal.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
//
// File.swift
// toucan
//
// Created by Viasz-Kádi Ferenc on 2024. 10. 17..
//

import Foundation

extension String {

/// Converts the string into a value of the specified data type.
///
/// - Parameters:
/// - dataType: The target data type to which the string should be converted.
/// - dateFormatter: The date formatter used to convert the string to a date if the data type is `date`.
/// - Returns: The converted value of the specified type, or nil if the conversion fails.
func value<T>(
for dataType: ContentType.Property.DataType,
dateFormatter: DateFormatter
) -> T? {
switch dataType {
case .bool:
return Bool(self) as? T
case .int:
return Int(self) as? T
case .double:
return Double(self) as? T
case .string:
return self as? T
case .date:
return dateFormatter.date(from: self) as? T
}
}
}

extension PageBundle {

/// Checks if a filter is satisfied based on the current page's front matter.
///
/// - Parameters:
/// - filter: The filter to check.
/// - dateFormatter: The date formatter used to parse date values.
/// - Returns: A boolean indicating whether the filter is satisfied.
func checkFilter(
_ filter: ContentType.Filter,
dateFormatter: DateFormatter
) -> Bool {
guard
let field = frontMatter[filter.field],
let dataType = contentType.properties?[filter.field]?.type,
let filterValue: Any = filter.value.value(
for: dataType,
dateFormatter: dateFormatter
)
else {
return false
}

switch filter.method {
case .equals:
return areValuesEqual(field, filterValue)
}
}
}

/// Compares two values of any type for equality.
///
/// - Parameters:
/// - lhs: The first value to compare.
/// - rhs: The second value to compare.
/// - Returns: A boolean indicating whether the two values are equal.
func areValuesEqual(_ lhs: Any, _ rhs: Any) -> Bool {
switch (lhs, rhs) {
case (let lhs as Bool, let rhs as Bool):
return lhs == rhs
case (let lhs as Int, let rhs as Int):
return lhs == rhs
case (let lhs as Double, let rhs as Double):
return lhs == rhs
case (let lhs as String, let rhs as String):
return lhs == rhs
case (let lhs as Date, let rhs as Date):
return lhs == rhs
case (let lhs as [Any], let rhs as [Any]):
return lhs.elementsEqual(rhs, by: { areValuesEqual($0, $1) })
case (let lhs as [String: Any], let rhs as [String: Any]):
return lhs.isEqualTo(rhs)
default:
return false
}
}

extension Dictionary where Key == String, Value == Any {

/// Compares two dictionaries for equality by checking their keys and values.
///
/// - Parameters:
/// - rhs: The dictionary to compare against.
/// - Returns: A boolean indicating whether the two dictionaries are equal.
func isEqualTo(_ rhs: [Key: Value]) -> Bool {
guard self.count == rhs.count else { return false }
for (key, value) in self {
guard let rhsValue = rhs[key] else {
return false
}
return areValuesEqual(value, rhsValue)
}
return true
}
}
61 changes: 61 additions & 0 deletions Sources/ToucanSDK/Extensions/PageBundle+Sort.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//
// File.swift
// toucan
//
// Created by Viasz-Kádi Ferenc on 2024. 10. 17..
//

import Foundation

extension PageBundle {

/// Compares two `PageBundle` instances for sorting based on a specified front matter key and order.
///
/// - Parameters:
/// - rhs: The `PageBundle` instance to compare with.
/// - frontMatterKey: The key in the front matter to use for comparison.
/// - order: The order (`asc` or `desc`) in which to perform the comparison.
/// - Returns: A Boolean value indicating whether the current `PageBundle` instance should be ordered before (`true`) or after (`false`) the rhs instance.
func compareForSorting(
for rhs: PageBundle,
frontMatterKey: String,
order: ContentType.Order
) -> Bool {
guard
let lhsField = frontMatter[frontMatterKey],
let rhsField = rhs.frontMatter[frontMatterKey]
else {
return false
}

switch order {
case .asc:
return compareValuesAscending(lhsField, rhsField)
case .desc:
return !compareValuesAscending(lhsField, rhsField)
}
}
}

/// Compares two values of any type in ascending order.
///
/// - Parameters:
/// - lhs: The first value to compare.
/// - rhs: The second value to compare.
/// - Returns: A Boolean value indicating whether the first value is less than the second value.
func compareValuesAscending(_ lhs: Any, _ rhs: Any) -> Bool {
switch (lhs, rhs) {
case let (lhs as Bool, rhs as Bool):
return !lhs && rhs
case let (lhs as Int, rhs as Int):
return lhs < rhs
case let (lhs as Double, rhs as Double):
return lhs < rhs
case let (lhs as String, rhs as String):
return lhs.caseInsensitiveCompare(rhs) == .orderedAscending
case let (lhs as Date, rhs as Date):
return lhs < rhs
default:
return String(describing: lhs) < String(describing: rhs)
}
}
Loading

0 comments on commit 9b3f8b4

Please sign in to comment.