mirror of
https://github.com/RevenueCat/purchases-ios
synced 2026-11-10 22:45:55 +05:45
* Add rc.sortBy custom operator Orders an array by a key computed per item, ascending, with ties keeping their input order. Keys must be all strings or all numbers so the two engines never have to invent a cross-type ordering. Co-authored-by: Cursor <cursoragent@cursor.com> * Add rc.slice custom operator Takes a run of elements out of an array, with the start and length semantics substr already uses for strings. Pairs with rc.sortBy to express top-N without a reduce carrying a compound accumulator. Co-authored-by: Cursor <cursoragent@cursor.com> * Exempt the custom operator dispatcher from cyclomatic complexity The tenth operator pushed the switch past the limit. Operators.dispatch carries the same exemption for the same reason. Co-authored-by: Cursor <cursoragent@cursor.com> * Reject non-finite rc.sortBy keys A NaN key compares false in both directions, so every pair tied and the input came back unsorted with no error. Negating a string key to attempt a descending sort produced exactly that. Co-authored-by: Cursor <cursoragent@cursor.com> * Note ascending order in the rc.sortBy summary Co-authored-by: Cursor <cursoragent@cursor.com> * Trim the rc.sortBy doc comment Co-authored-by: Cursor <cursoragent@cursor.com> * Trim the rc.sortBy stability comment Co-authored-by: Cursor <cursoragent@cursor.com> * Drop the dispatcher comment above the lint annotation Co-authored-by: Cursor <cursoragent@cursor.com> * Scope the substr hint to string input in rc.slice Co-authored-by: Cursor <cursoragent@cursor.com> * Pin rc.slice index and source type strictness in fixtures Co-authored-by: Cursor <cursoragent@cursor.com> * Reach the float index branch through arithmetic in rc.slice fixtures Co-authored-by: Cursor <cursoragent@cursor.com> * Order rc.sortBy string keys by UTF-16 code unit Swift's < orders by Unicode scalar and treats canonically equivalent strings as equal, while JS and Kotlin order by UTF-16 code unit. The two disagree once a scalar above the surrogate range meets an astral one, so the same rule would sort differently on iOS and Android. Co-authored-by: Cursor <cursoragent@cursor.com> * Keep the rc.sortBy ordering fixtures platform-neutral The fixture files are byte-identical across the iOS and Android engines, so a description that explains the contract in terms of one host language's string comparison does not read correctly in the other repository. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
79 lines
3.1 KiB
Swift
79 lines
3.1 KiB
Swift
//
|
|
// SliceOperator.swift
|
|
//
|
|
// Created by Antonio Pallares.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
/// Name used in this operator's error messages.
|
|
private let operatorName = "rc.slice"
|
|
|
|
extension RulesEngine {
|
|
|
|
/// `rc.slice` — takes a run of elements out of an array.
|
|
enum SliceOperator {
|
|
|
|
/// `{"rc.slice": [array, start]}` or
|
|
/// `{"rc.slice": [array, start, length]}`.
|
|
///
|
|
/// Start and length mean what they mean in `substr`, the string operator
|
|
/// already in the engine: a negative `start` counts from the end, a
|
|
/// negative `length` drops that many elements from the right, and both
|
|
/// clamp to the array instead of failing. Strings keep using `substr`.
|
|
///
|
|
/// Indices must be whole numbers — `.float` is accepted because all
|
|
/// arithmetic returns it, so `length - 1` is an ordinary way to reach the
|
|
/// last element. A fractional or non-numeric index throws
|
|
/// `EvaluationError.typeMismatch`, as does a non-array operand.
|
|
static func opSlice(args: Value, vars: Scope) throws -> Value {
|
|
let evaluated = try Operators.evalArgs(args, vars: vars)
|
|
|
|
guard evaluated.count == 2 || evaluated.count == 3 else {
|
|
throw EvaluationError.typeMismatch(
|
|
message: "operator '\(operatorName)' expects 2 or 3 arguments, "
|
|
+ "got \(evaluated.count)"
|
|
)
|
|
}
|
|
|
|
guard case .array(let items) = evaluated[0] else {
|
|
var hint = ""
|
|
if case .string = evaluated[0] {
|
|
hint = "; strings use 'substr'"
|
|
}
|
|
throw EvaluationError.typeMismatch(
|
|
message: "operator '\(operatorName)' expected an array to slice, "
|
|
+ "got \(evaluated[0])\(hint)"
|
|
)
|
|
}
|
|
|
|
let start = try Self.index(evaluated[1], argument: "start")
|
|
let begin = start < 0 ? max(items.count + start, 0) : min(start, items.count)
|
|
let remaining = Array(items[begin...])
|
|
|
|
guard evaluated.count == 3 else { return .array(remaining) }
|
|
|
|
let length = try Self.index(evaluated[2], argument: "length")
|
|
let count = length < 0
|
|
? max(remaining.count + length, 0)
|
|
: min(length, remaining.count)
|
|
return .array(Array(remaining[..<count]))
|
|
}
|
|
|
|
/// Reads a whole-number index, rejecting anything that is not one.
|
|
private static func index(_ value: Value, argument: String) throws -> Int {
|
|
switch value {
|
|
case .int(let number):
|
|
return Int(clamping: number)
|
|
case .float(let number) where number.truncatingRemainder(dividingBy: 1) == 0:
|
|
return Operators.clampedInt(number)
|
|
default:
|
|
throw EvaluationError.typeMismatch(
|
|
message: "operator '\(operatorName)' expected a whole number "
|
|
+ "\(argument), got \(value)"
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|