├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── Tests └── KoalasTests │ ├── XCTestManifests.swift │ ├── DataPanelTests.swift │ ├── DataSeriesAdvancedTests.swift │ ├── DataFrameIOTests.swift │ ├── DataFrameAdvancedTests.swift │ ├── UtilityTests.swift │ ├── ArithmeticOperatorTests.swift │ └── DataSeriesTests.swift ├── Sources └── Koalas │ ├── DataFrame │ ├── DataFrame+Date.swift │ ├── DataFrameType.swift │ ├── DataFrame+IO.swift │ ├── DataFrame.swift │ └── DataFrame+Arithmetics.swift │ ├── DataSeries │ ├── FillNilsMethod.swift │ ├── DataSeriesType.swift │ ├── Tuple.swift │ ├── UnwrapUtils.swift │ ├── SeriesArray+Date.swift │ ├── SeriesArray.swift │ ├── DataSeries.swift │ ├── SeriesArrayExtensions.swift │ └── DataSeries+Arithmetics.swift │ └── DataPanel │ └── DataPanel.swift ├── LICENSE ├── Package.swift ├── logo.svg └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Tests/KoalasTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase([]), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /Sources/Koalas/DataFrame/DataFrame+Date.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Sergey Kazakov on 23.06.2020. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension DataFrame where Value == DataSeries { 11 | /** 12 | Converts a DataFrame containing Date DataSeries into a DataPanel with DateComponents. 13 | Extracts individual date components (year, month, day, hour, minute, second, etc.) 14 | from each date series and organizes them into a structured panel format. 15 | */ 16 | func toDateComponents() -> DataPanel { 17 | return upscaleTransform { $0.toDateComponents() } 18 | } 19 | } 20 | 21 | 22 | -------------------------------------------------------------------------------- /Sources/Koalas/DataSeries/FillNilsMethod.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Sergey Kazakov on 11.06.2020. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Defines methods for filling nil values in DataSeries. 12 | Provides different strategies for handling missing data in time series or sequential data. 13 | 14 | - all: Fills all nil values with a constant value. Replaces every nil element with the specified value regardless of position. 15 | 16 | - backward: Fills nil values using backward fill strategy. 17 | Propagates the last known value backward to fill preceding nil values. Uses the specified initial value for the first nil values encountered. 18 | 19 | - forward: Fills nil values using forward fill strategy. 20 | Propagates the last known value forward to fill succeeding nil values. Uses the specified initial value for the first nil values encountered. 21 | */ 22 | public enum FillNilsMethod { 23 | case all(value: T) 24 | case backward(initial: T) 25 | case forward(initial: T) 26 | } 27 | -------------------------------------------------------------------------------- /Sources/Koalas/DataSeries/DataSeriesType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Sergey Kazakov on 07.07.2020. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | A type that can represent either a DataSeries or a scalar value. 12 | Used for conditional operations where the result can be either a full DataSeries 13 | or a constant value applied across all elements. 14 | */ 15 | public enum DataSeriesType { 16 | case ds(DS?) 17 | case value(V?) 18 | 19 | /** 20 | Converts this DataSeriesType to a DataSeries with the same shape as the reference series. 21 | If this is a scalar value, it creates a DataSeries filled with that value. 22 | If this is already a DataSeries, it returns it directly. 23 | */ 24 | func toDataSeriesWithShape(of series: DataSeries) -> DataSeries? where DS == DataSeries { 25 | switch self { 26 | case .ds(let dataSeries): 27 | return dataSeries 28 | case .value(let scalarValue): 29 | return DataSeries(series.map { _ in return scalarValue }) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/Koalas/DataFrame/DataFrameType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Sergey Kazakov on 25.06.2020. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | A type that can represent either a DataFrame or a scalar value. 12 | Used for conditional operations where the result can be either a full DataFrame 13 | or a constant value applied across all elements. 14 | */ 15 | public enum DataFrameType { 16 | case df(DF?) 17 | case value(V?) 18 | 19 | /** 20 | Converts this DataFrameType to a DataFrame with the same shape as the reference DataFrame. 21 | If this is a scalar value, it creates a DataFrame filled with that value. 22 | If this is already a DataFrame, it returns it directly. 23 | */ 24 | func toDataframeWithShape(of dataframe: DataFrame) -> DataFrame? where DF == DataFrame { 25 | switch self { 26 | case .df(let df): 27 | return df 28 | case .value(let scalarValue): 29 | return dataframe.mapValues { DataSeries($0.map { _ in return scalarValue }) } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Kazakov Sergey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Koalas", 8 | platforms: [ 9 | .macOS(.v10_15), 10 | .iOS(.v8) 11 | ], 12 | 13 | products: [ 14 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 15 | .library( 16 | name: "Koalas", 17 | targets: ["Koalas"]), 18 | ], 19 | dependencies: [ 20 | // Dependencies declare other packages that this package depends on. 21 | // .package(url: /* package url */, from: "1.0.0"), 22 | ], 23 | targets: [ 24 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 25 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 26 | .target( 27 | name: "Koalas", 28 | dependencies: []), 29 | .testTarget( 30 | name: "KoalasTests", 31 | dependencies: ["Koalas"]), 32 | ] 33 | ) 34 | -------------------------------------------------------------------------------- /Sources/Koalas/DataSeries/Tuple.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Sergey Kazakov on 03.07.2020. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | A tuple structure containing two elements of different types. 12 | Used for combining two DataSeries into a single series of paired values. 13 | Both elements must conform to Codable for serialization support. 14 | */ 15 | public struct Tuple2: Codable { 16 | public let t1: T1 17 | public let t2: T2 18 | 19 | public init(t1: T1, t2: T2) { 20 | self.t1 = t1 21 | self.t2 = t2 22 | } 23 | } 24 | 25 | /** 26 | A tuple structure containing three elements of different types. 27 | Used for combining three DataSeries into a single series of grouped values. 28 | All elements must conform to Codable for serialization support. 29 | */ 30 | public struct Tuple3: Codable { 31 | public let t1: T1 32 | public let t2: T2 33 | public let t3: T3 34 | 35 | public init(t1: T1, t2: T2, t3: T3) { 36 | self.t1 = t1 37 | self.t2 = t2 38 | self.t3 = t3 39 | } 40 | } 41 | 42 | /** 43 | A tuple structure containing four elements of different types. 44 | Used for combining four DataSeries into a single series of grouped values. 45 | All elements must conform to Codable for serialization support. 46 | */ 47 | public struct Tuple4: Codable { 48 | public let t1: T1 49 | public let t2: T2 50 | public let t3: T3 51 | public let t4: T4 52 | 53 | public init(t1: T1, t2: T2, t3: T3, t4: T4) { 54 | self.t1 = t1 55 | self.t2 = t2 56 | self.t3 = t3 57 | self.t4 = t4 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/Koalas/DataSeries/UnwrapUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Sergey Kazakov on 25.06.2020. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Safely unwraps two optional values and applies a transformation function. 12 | Returns nil if either value is nil, otherwise applies the map function to the unwrapped values. 13 | */ 14 | public func unwrap(_ lhs: T?, _ rhs: T?, map: (T, T) -> U) -> U? { 15 | guard let lhs = lhs, let rhs = rhs else { 16 | return nil 17 | } 18 | 19 | return map(lhs, rhs) 20 | } 21 | 22 | /** 23 | Safely unwraps two optional values of different types and applies a transformation function. 24 | Returns nil if either value is nil, otherwise applies the map function to the unwrapped values. 25 | */ 26 | public func unwrap(_ lhs: T?, _ rhs: U?, map: (T, U) -> V?) -> V? { 27 | guard let lhs = lhs, let rhs = rhs else { 28 | return nil 29 | } 30 | 31 | return map(lhs, rhs) 32 | } 33 | 34 | /** 35 | Safely unwraps three optional values and applies a transformation function. 36 | Returns nil if any value is nil, otherwise applies the map function to the unwrapped values. 37 | */ 38 | public func unwrap(_ t: T?, _ u: U?, _ v: V?, map: (T, U, V) -> S?) -> S? { 39 | guard let t = t, let u = u, let v = v else { 40 | return nil 41 | } 42 | 43 | return map(t, u, v) 44 | } 45 | 46 | /** 47 | Safely unwraps a single optional value and applies a transformation function. 48 | Returns nil if the value is nil, otherwise applies the map function to the unwrapped value. 49 | */ 50 | public func unwrap(value: T?, map: (T) -> U?) -> U? { 51 | guard let value = value else { 52 | return nil 53 | } 54 | 55 | return map(value) 56 | } 57 | -------------------------------------------------------------------------------- /Sources/Koalas/DataSeries/SeriesArray+Date.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Sergey Kazakov on 23.06.2020. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Defines the keys for date components when extracting parts of dates. 12 | Used to organize date components (year, month, day) in a structured format. 13 | */ 14 | public enum DateComponentsKeys: String { 15 | case year 16 | case month 17 | case day 18 | } 19 | 20 | public extension SeriesArray where Element == Date? { 21 | /** 22 | Converts a SeriesArray of dates into a DataFrame containing individual date components. 23 | Extracts year, month, and day from each date and organizes them into separate DataSeries. 24 | Returns a DataFrame with three columns: year, month, and day, each containing integer values. 25 | */ 26 | func toDateComponents() -> DataFrame { 27 | let yearDateFormatter = DateFormatter() 28 | yearDateFormatter.dateFormat = "yyyy" 29 | 30 | let monthDateFormatter = DateFormatter() 31 | monthDateFormatter.dateFormat = "MM" 32 | 33 | let dayDateFormatter = DateFormatter() 34 | dayDateFormatter.dateFormat = "dd" 35 | 36 | let yearSeries = DataSeries(map { unwrap(value: $0) { date in Int(yearDateFormatter.string(from: date)) } }) 37 | let monthSeries = DataSeries(map { unwrap(value: $0) { date in Int(monthDateFormatter.string(from: date)) } }) 38 | let daySeries = DataSeries(map { unwrap(value: $0) { date in Int(dayDateFormatter.string(from: date)) } }) 39 | 40 | return DataFrame(uniqueKeysWithValues: [(.year, yearSeries), 41 | (.month, monthSeries), 42 | (.day, daySeries)]) 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /Sources/Koalas/DataPanel/DataPanel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Sergey Kazakov on 04.06.2020. 6 | // 7 | 8 | /** 9 | A dictionary-like structure that maps keys to DataFrames. 10 | Used to store and manipulate 3D tabular data with multiple keys and columns. 11 | */ 12 | public typealias DataPanel = [Key: DataFrame] 13 | 14 | public extension DataPanel { 15 | /** 16 | Transposes the DataPanel by swapping the key dimensions. 17 | Converts from [Key1: DataFrame] to [Key2: DataFrame]. 18 | Useful for restructuring data from wide to long format or vice versa. 19 | */ 20 | func transposed() -> DataPanel 21 | where Value == DataFrame { 22 | 23 | var transposedData: [Key2: DataFrame] = [:] 24 | self.forEach { 25 | let key1 = $0.key 26 | $0.value.forEach { 27 | let key2 = $0.key 28 | let value = $0.value 29 | var df = transposedData[key2] ?? DataFrame() 30 | df[key1] = value 31 | transposedData[key2] = df 32 | } 33 | } 34 | 35 | return transposedData 36 | } 37 | 38 | /** 39 | Applies a transformation function to each DataSeries within all DataFrames in the DataPanel. 40 | Returns a new DataPanel with transformed DataSeries while preserving the structure. 41 | */ 42 | func flatMapDataFrameValues(_ transform: (DataSeries) -> DataSeries ) -> DataPanel where Value == DataFrame { 43 | return self.mapValues { $0.mapValues { transform($0) } } 44 | } 45 | 46 | /** 47 | Applies a transformation function to each individual value within all DataSeries in the DataPanel. 48 | Handles nil values and returns a new DataPanel with transformed values. 49 | */ 50 | func flatMapValues(_ transform: (V?) -> U? ) -> DataPanel where Value == DataFrame { 51 | return self.flatMapDataFrameValues { series in DataSeries(series.map { transform($0) }) } 52 | } 53 | 54 | /** 55 | Applies a transformation function to two specific DataFrames in the DataPanel. 56 | Takes two keys and a transformation function that operates on the corresponding DataFrames. 57 | Returns a single DataFrame as the result of the transformation. 58 | */ 59 | func mapValues(keys: (Key, Key), 60 | transform: (DataFrame?, DataFrame?) -> DataFrame) 61 | -> DataFrame 62 | 63 | where 64 | Value == DataFrame { 65 | 66 | return transform(self[keys.0], self[keys.1]) 67 | } 68 | 69 | /** 70 | Returns the dimensions of the DataPanel as (depth, width, height). 71 | Depth is the number of top-level keys, width and height are from the contained DataFrames. 72 | */ 73 | func shape() -> (depth: Int, width: Int, height: Int) where Value == DataFrame { 74 | let valueShape = self.values.first?.shape() ?? (width: 0, height: 0) 75 | return (self.keys.count, valueShape.width, valueShape.height) 76 | } 77 | } 78 | 79 | 80 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /Sources/Koalas/DataFrame/DataFrame+IO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Sergey Kazakov on 23.06.2020. 6 | // 7 | 8 | import Foundation 9 | 10 | extension DataFrame { 11 | /** 12 | Converts the DataFrame to an array of string lines representing the data in tabular format. 13 | The first line contains column headers (keys), followed by data rows. 14 | Each row is joined with the specified separator. 15 | */ 16 | public func toStringRowLines(separator: String) -> [String] where Value == DataSeries, V: LosslessStringConvertible, Key: LosslessStringConvertible { 17 | var resultStringLines: [String] = [] 18 | let sortedKeys = keys.sorted { return String($0) < String($1) } 19 | 20 | resultStringLines.append("\(sortedKeys.map { String($0) }.joined(separator: separator))") 21 | let sortedValues = sortedKeys.map { self[$0] } 22 | 23 | let height = shape().height 24 | for idx in 0..(toFile: String, 38 | atomically: Bool = true, 39 | encoding: String.Encoding = .utf8, 40 | columnSeparator: String) throws 41 | where 42 | 43 | Value == DataSeries, 44 | V: LosslessStringConvertible, 45 | Key: LosslessStringConvertible { 46 | 47 | let dataframeString = toStringRowLines(separator: columnSeparator).joined(separator: "\n") 48 | try dataframeString.write(toFile: toFile, atomically: atomically, encoding: encoding) 49 | } 50 | 51 | /** 52 | Initializes a DataFrame from a file containing tabular data. 53 | The first line is expected to contain column headers, followed by data rows. 54 | Uses the specified encoding and column separator to parse the file. 55 | */ 56 | public init( 57 | contentsOfFile file: String, 58 | encoding: String.Encoding = .utf8, 59 | columnSeparator: String) throws 60 | 61 | where 62 | Value == DataSeries, 63 | V: LosslessStringConvertible, 64 | Key: LosslessStringConvertible, 65 | Key: Hashable { 66 | 67 | self = try Self.read(from: file, encoding: encoding, columnSeparator: columnSeparator) 68 | } 69 | 70 | /** 71 | Reads a DataFrame from a file and parses it according to the specified format. 72 | Expects the first line to contain column headers and subsequent lines to contain data. 73 | Returns a DataFrame with the parsed data structure. 74 | */ 75 | fileprivate static func read( 76 | from file: String, 77 | encoding: String.Encoding = .utf8, 78 | columnSeparator: String) throws -> DataFrame 79 | 80 | where 81 | Value == DataSeries, 82 | V: LosslessStringConvertible, 83 | K: LosslessStringConvertible, 84 | K: Hashable { 85 | 86 | let fileString = try String(contentsOfFile: file, encoding: encoding) 87 | 88 | var df = DataFrame() 89 | var keys: [K] = [] 90 | 91 | var lineNumber = 0 92 | 93 | fileString.enumerateLines { (line, _) in 94 | let lineComponents = line.components(separatedBy: columnSeparator) 95 | if lineNumber == 0 { 96 | keys = lineComponents 97 | .map { K($0) } 98 | .compactMap { $0 } 99 | 100 | keys.forEach { df[$0] = DataSeries() } 101 | } else { 102 | let valuesRow = lineComponents.map { V($0) } 103 | zip(keys, valuesRow).forEach { df[$0]?.append($01) } 104 | } 105 | 106 | lineNumber += 1 107 | } 108 | 109 | return df 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Sources/Koalas/DataSeries/SeriesArray.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Sergey Kazakov on 06.06.2020. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | A generic array structure that conforms to RangeReplaceableCollection and Codable. 12 | Serves as the underlying data structure for DataSeries, providing a type-safe, 13 | serializable collection that supports all standard array operations including 14 | filtering, insertion, removal, and range replacement. 15 | */ 16 | public struct SeriesArray: RangeReplaceableCollection, Codable { 17 | 18 | public typealias Element = T 19 | public typealias Index = Int 20 | public typealias SubSequence = SeriesArray 21 | public typealias Indices = Range 22 | fileprivate var array: Array 23 | 24 | public var startIndex: Int { return array.startIndex } 25 | public var endIndex: Int { return array.endIndex } 26 | public var indices: Range { return array.indices } 27 | 28 | 29 | public func index(after i: Int) -> Int { 30 | return array.index(after: i) 31 | } 32 | 33 | public init() { array = [] } 34 | } 35 | 36 | // Instance Methods 37 | 38 | public extension SeriesArray { 39 | 40 | init(_ elements: S) where S : Sequence, SeriesArray.Element == S.Element { 41 | array = Array(elements) 42 | } 43 | 44 | init(repeating repeatedValue: SeriesArray.Element, count: Int) { 45 | array = Array(repeating: repeatedValue, count: count) 46 | } 47 | } 48 | 49 | // Instance Methods 50 | 51 | public extension SeriesArray { 52 | 53 | mutating func append(_ newElement: SeriesArray.Element) { 54 | array.append(newElement) 55 | } 56 | 57 | mutating func append(contentsOf newElements: S) where S : Sequence, SeriesArray.Element == S.Element { 58 | array.append(contentsOf: newElements) 59 | } 60 | 61 | func filter(_ isIncluded: (SeriesArray.Element) throws -> Bool) rethrows -> SeriesArray { 62 | let subArray = try array.filter(isIncluded) 63 | return SeriesArray(subArray) 64 | } 65 | 66 | mutating func insert(_ newElement: SeriesArray.Element, at i: SeriesArray.Index) { 67 | array.insert(newElement, at: i) 68 | } 69 | 70 | mutating func insert(contentsOf newElements: S, at i: SeriesArray.Index) where S : Collection, SeriesArray.Element == S.Element { 71 | array.insert(contentsOf: newElements, at: i) 72 | } 73 | 74 | mutating func popLast() -> SeriesArray.Element? { 75 | return array.popLast() 76 | } 77 | 78 | @discardableResult mutating func remove(at i: SeriesArray.Index) -> SeriesArray.Element { 79 | return array.remove(at: i) 80 | } 81 | 82 | mutating func removeAll(keepingCapacity keepCapacity: Bool) { 83 | array.removeAll() 84 | } 85 | 86 | mutating func removeAll(where shouldBeRemoved: (SeriesArray.Element) throws -> Bool) rethrows { 87 | try array.removeAll(where: shouldBeRemoved) 88 | } 89 | 90 | @discardableResult mutating func removeFirst() -> SeriesArray.Element { 91 | return array.removeFirst() 92 | } 93 | 94 | mutating func removeFirst(_ k: Int) { 95 | array.removeFirst(k) 96 | } 97 | 98 | @discardableResult mutating func removeLast() -> SeriesArray.Element { 99 | return array.removeLast() 100 | } 101 | 102 | mutating func removeLast(_ k: Int) { 103 | array.removeLast(k) 104 | } 105 | 106 | mutating func removeSubrange(_ bounds: Range) { 107 | array.removeSubrange(bounds) 108 | } 109 | 110 | mutating func replaceSubrange(_ subrange: R, with newElements: C) where C : Collection, R : RangeExpression, T == C.Element, SeriesArray.Index == R.Bound { 111 | array.replaceSubrange(subrange, with: newElements) 112 | } 113 | 114 | mutating func reserveCapacity(_ n: Int) { 115 | array.reserveCapacity(n) 116 | } 117 | } 118 | 119 | // Subscripts 120 | 121 | public extension SeriesArray { 122 | 123 | subscript(bounds: Range) -> SeriesArray.SubSequence { 124 | get { return SeriesArray(array[bounds]) } 125 | } 126 | 127 | subscript(bounds: SeriesArray.Index) -> SeriesArray.Element { 128 | get { return array[bounds] } 129 | set(value) { array[bounds] = value } 130 | } 131 | } 132 | 133 | extension SeriesArray: CustomStringConvertible { 134 | public var description: String { return "\(array)" } 135 | } 136 | -------------------------------------------------------------------------------- /Sources/Koalas/DataSeries/DataSeries.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Sergey Kazakov on 04.06.2020. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | A SeriesArray of optional values that can be encoded and decoded using Codable. 12 | Used to store and manipulate 1D tabular data with optional values. 13 | */ 14 | public typealias DataSeries = SeriesArray 15 | 16 | /** 17 | Applies a conditional operation to DataSeries based on a boolean condition. 18 | Returns the true DataSeries where condition is true, false DataSeries where condition is false. 19 | Handles DataSeriesType which can be either a DataSeries or a scalar value. 20 | */ 21 | public func whereCondition(_ condition: DataSeries?, 22 | then trueSeries: DataSeriesType, U>, 23 | else series: DataSeriesType, U>) -> DataSeries? { 24 | 25 | guard let condition = condition else { 26 | return nil 27 | } 28 | 29 | guard let trueDS = trueSeries.toDataSeriesWithShape(of: condition) else { 30 | return nil 31 | } 32 | 33 | guard let falseDS = series.toDataSeriesWithShape(of: condition) else { 34 | return nil 35 | } 36 | 37 | return whereCondition(condition, then: trueDS, else: falseDS) 38 | } 39 | 40 | /** 41 | Applies a conditional operation to DataSeries based on a boolean condition. 42 | Returns the true DataSeries where condition is true, false DataSeries where condition is false. 43 | */ 44 | public func whereCondition(_ condition: DataSeries?, then trueSeries: DataSeries?, else series: DataSeries?) -> DataSeries? { 45 | return condition?.whereTrue(then: trueSeries, else: series) 46 | } 47 | 48 | /** 49 | Applies a conditional operation to DataSeries based on a boolean condition. 50 | Returns a DataSeries with trueValue where condition is true, value where condition is false. 51 | */ 52 | public func whereCondition(_ condition: DataSeries?, then trueValue: U, else value: U) -> DataSeries? { 53 | guard let condition = condition else { 54 | return nil 55 | } 56 | 57 | let trueSeries = condition.mapTo(constant: trueValue) 58 | let falseSeries = condition.mapTo(constant: value) 59 | 60 | return condition.whereTrue(then: trueSeries, else: falseSeries) 61 | } 62 | 63 | /** 64 | Combines three DataSeries into a single DataSeries of tuples. 65 | Returns nil if any of the input series are nil or have different lengths. 66 | Each element in the result is a Tuple3 containing corresponding elements from the input series. 67 | */ 68 | public func zipSeries(_ s1: DataSeries?, _ s2: DataSeries?, _ s3: DataSeries?) -> DataSeries>? { 69 | guard let s1 = s1, 70 | let s2 = s2, 71 | let s3 = s3 72 | else { 73 | return nil 74 | } 75 | 76 | assert(s1.count == s2.count, "Dataseries should have equal length") 77 | assert(s1.count == s3.count, "Dataseries should have equal length") 78 | 79 | let result = zip(s1, zip(s2, s3)).map { Tuple3(t1: $0.0, t2: $0.1.0, t3: $0.1.1) } 80 | return DataSeries(result) 81 | } 82 | 83 | /** 84 | Combines four DataSeries into a single DataSeries of tuples. 85 | Returns nil if any of the input series are nil or have different lengths. 86 | Each element in the result is a Tuple4 containing corresponding elements from the input series. 87 | */ 88 | public func zipSeries(_ s1: DataSeries?, _ s2: DataSeries?, _ s3: DataSeries?, _ s4: DataSeries?) -> DataSeries>? { 89 | guard let s1 = s1, 90 | let s2 = s2, 91 | let s3 = s3, 92 | let s4 = s4 93 | else { 94 | return nil 95 | } 96 | 97 | assert(s1.count == s2.count, "Dataseries should have equal length") 98 | assert(s1.count == s3.count, "Dataseries should have equal length") 99 | assert(s1.count == s4.count, "Dataseries should have equal length") 100 | 101 | 102 | let result = zip(zip(s1, s2), zip(s3, s4)).map { Tuple4(t1: $0.0.0, t2: $0.0.1, t3: $0.1.0, t4: $0.1.1) } 103 | return DataSeries(result) 104 | } 105 | 106 | /** 107 | Combines two DataSeries into a single DataSeries of tuples. 108 | Returns nil if any of the input series are nil or have different lengths. 109 | Each element in the result is a Tuple2 containing corresponding elements from the input series. 110 | */ 111 | public func zipSeries(_ s1: DataSeries?, _ s2: DataSeries?) -> DataSeries>? { 112 | guard let s1 = s1, 113 | let s2 = s2 114 | else { 115 | return nil 116 | } 117 | 118 | assert(s1.count == s2.count, "Dataseries should have equal length") 119 | 120 | let result = zip(s1, s2).map { Tuple2(t1: $0.0, t2: $0.1) } 121 | return DataSeries(result) 122 | } 123 | -------------------------------------------------------------------------------- /Tests/KoalasTests/DataPanelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataPanelTests.swift 3 | // 4 | // 5 | // Created by AI Assistant on 2024. 6 | // 7 | 8 | import XCTest 9 | @testable import Koalas 10 | 11 | final class DataPanelTests: XCTestCase { 12 | 13 | // MARK: - transposed Tests 14 | 15 | func test_transposed_SwapsKeyDimensions() { 16 | let s1 = DataSeries([1, 2, 3]) 17 | let s2 = DataSeries([4, 5, 6]) 18 | let s3 = DataSeries([7, 8, 9]) 19 | let s4 = DataSeries([10, 11, 12]) 20 | 21 | let df1 = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 22 | let df2 = DataFrame(dictionaryLiteral: ("col1", s3), ("col2", s4)) 23 | 24 | let panel: DataPanel = [ 25 | "row1": df1, 26 | "row2": df2 27 | ] 28 | 29 | let transposed = panel.transposed() 30 | 31 | // Check that keys are swapped 32 | XCTAssertEqual(transposed.keys.count, 2) 33 | XCTAssertTrue(transposed.keys.contains("col1")) 34 | XCTAssertTrue(transposed.keys.contains("col2")) 35 | 36 | // Check that values are correctly transposed 37 | XCTAssertEqual(transposed["col1"]?["row1"]?[0], 1) 38 | XCTAssertEqual(transposed["col1"]?["row2"]?[0], 7) 39 | XCTAssertEqual(transposed["col2"]?["row1"]?[0], 4) 40 | XCTAssertEqual(transposed["col2"]?["row2"]?[0], 10) 41 | } 42 | 43 | func test_transposed_WithEmptyPanel() { 44 | let panel: DataPanel = [:] 45 | 46 | let transposed = panel.transposed() 47 | 48 | XCTAssertEqual(transposed.count, 0) 49 | } 50 | 51 | func test_transposed_WithEmptyDataFrames() { 52 | let df1: DataFrame = [:] 53 | let df2: DataFrame = [:] 54 | 55 | let panel: DataPanel = [ 56 | "row1": df1, 57 | "row2": df2 58 | ] 59 | 60 | let transposed = panel.transposed() 61 | 62 | XCTAssertEqual(transposed.count, 0) 63 | } 64 | 65 | // MARK: - flatMapDataFrameValues Tests 66 | 67 | func test_flatMapDataFrameValues_TransformsDataSeries() { 68 | let s1 = DataSeries([1, 2, 3]) 69 | let s2 = DataSeries([4, 5, 6]) 70 | let s3 = DataSeries([7, 8, 9]) 71 | let s4 = DataSeries([10, 11, 12]) 72 | 73 | let df1 = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 74 | let df2 = DataFrame(dictionaryLiteral: ("col1", s3), ("col2", s4)) 75 | 76 | let panel: DataPanel = [ 77 | "row1": df1, 78 | "row2": df2 79 | ] 80 | 81 | let result = panel.flatMapDataFrameValues { series in 82 | DataSeries(series.map { $0.map { $0 * 2 } }) 83 | } 84 | 85 | XCTAssertEqual(result["row1"]?["col1"]?[0], 2) 86 | XCTAssertEqual(result["row1"]?["col1"]?[1], 4) 87 | XCTAssertEqual(result["row1"]?["col2"]?[0], 8) 88 | XCTAssertEqual(result["row2"]?["col1"]?[0], 14) 89 | XCTAssertEqual(result["row2"]?["col2"]?[0], 20) 90 | } 91 | 92 | func test_flatMapDataFrameValues_HandlesNilValues() { 93 | let s1 = DataSeries([1, nil, 3]) 94 | let s2 = DataSeries([4, 5, nil]) 95 | 96 | let df1 = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 97 | 98 | let panel: DataPanel = [ 99 | "row1": df1 100 | ] 101 | 102 | let result = panel.flatMapDataFrameValues { series in 103 | DataSeries(series.map { ($0 ?? 0) * 2 }) 104 | } 105 | 106 | XCTAssertEqual(result["row1"]?["col1"]?[0], 2) 107 | XCTAssertEqual(result["row1"]?["col1"]?[1], 0) 108 | XCTAssertEqual(result["row1"]?["col1"]?[2], 6) 109 | XCTAssertEqual(result["row1"]?["col2"]?[0], 8) 110 | XCTAssertEqual(result["row1"]?["col2"]?[1], 10) 111 | XCTAssertEqual(result["row1"]?["col2"]?[2], 0) 112 | } 113 | 114 | // MARK: - flatMapValues Tests 115 | 116 | func test_flatMapValues_TransformsIndividualValues() { 117 | let s1 = DataSeries([1, 2, 3]) 118 | let s2 = DataSeries([4, 5, 6]) 119 | 120 | let df1 = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 121 | 122 | let panel: DataPanel = [ 123 | "row1": df1 124 | ] 125 | 126 | let result = panel.flatMapValues { value in 127 | value.map { $0 * 2 } 128 | } 129 | 130 | XCTAssertEqual(result["row1"]?["col1"]?[0], 2) 131 | XCTAssertEqual(result["row1"]?["col1"]?[1], 4) 132 | XCTAssertEqual(result["row1"]?["col1"]?[2], 6) 133 | XCTAssertEqual(result["row1"]?["col2"]?[0], 8) 134 | XCTAssertEqual(result["row1"]?["col2"]?[1], 10) 135 | XCTAssertEqual(result["row1"]?["col2"]?[2], 12) 136 | } 137 | 138 | func test_flatMapValues_HandlesNilValues() { 139 | let s1 = DataSeries([1, nil, 3]) 140 | let s2 = DataSeries([4, 5, nil]) 141 | 142 | let df1 = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 143 | 144 | let panel: DataPanel = [ 145 | "row1": df1 146 | ] 147 | 148 | let result = panel.flatMapValues { value in 149 | value.map { $0 * 2 } 150 | } 151 | 152 | XCTAssertEqual(result["row1"]?["col1"]?[0], 2) 153 | XCTAssertNil(result["row1"]?["col1"]?[1]) 154 | XCTAssertEqual(result["row1"]?["col1"]?[2], 6) 155 | XCTAssertEqual(result["row1"]?["col2"]?[0], 8) 156 | XCTAssertEqual(result["row1"]?["col2"]?[1], 10) 157 | XCTAssertNil(result["row1"]?["col2"]?[2]) 158 | } 159 | 160 | // MARK: - mapValues Tests 161 | 162 | func test_mapValues_WithTwoKeys() { 163 | let s1 = DataSeries([1, 2, 3]) 164 | let s2 = DataSeries([4, 5, 6]) 165 | let s3 = DataSeries([7, 8, 9]) 166 | 167 | let df1 = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 168 | let df2 = DataFrame(dictionaryLiteral: ("col1", s2), ("col2", s3)) 169 | 170 | let panel: DataPanel = [ 171 | "row1": df1, 172 | "row2": df2 173 | ] 174 | 175 | let result = panel.mapValues(keys: ("row1", "row2")) { df1, df2 in 176 | guard let df1 = df1, let df2 = df2 else { return DataFrame() } 177 | return df1 + df2 178 | } 179 | 180 | XCTAssertEqual(result["col1"]?[0], 5) // 1 + 4 181 | XCTAssertEqual(result["col1"]?[1], 7) // 2 + 5 182 | XCTAssertEqual(result["col1"]?[2], 9) // 3 + 6 183 | XCTAssertEqual(result["col2"]?[0], 11) // 4 + 7 184 | XCTAssertEqual(result["col2"]?[1], 13) // 5 + 8 185 | XCTAssertEqual(result["col2"]?[2], 15) // 6 + 9 186 | } 187 | 188 | func test_mapValues_WithNilDataFrames() { 189 | let s1 = DataSeries([1, 2, 3]) 190 | 191 | let df1 = DataFrame(dictionaryLiteral: ("col1", s1)) 192 | 193 | let panel: DataPanel = [ 194 | "row1": df1 195 | ] 196 | 197 | let result = panel.mapValues(keys: ("row1", "nonexistent")) { df1, df2 in 198 | guard let df1 = df1, let df2 = df2 else { return DataFrame() } 199 | return df1 + df2 200 | } 201 | 202 | XCTAssertEqual(result.count, 0) // Should return empty DataFrame 203 | } 204 | 205 | // MARK: - shape Tests 206 | 207 | func test_shape_ReturnsCorrectDimensions() { 208 | let s1 = DataSeries([1, 2, 3]) 209 | let s2 = DataSeries([4, 5, 6]) 210 | let s3 = DataSeries([7, 8, 9]) 211 | let s4 = DataSeries([10, 11, 12]) 212 | 213 | let df1 = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 214 | let df2 = DataFrame(dictionaryLiteral: ("col1", s3), ("col2", s4)) 215 | 216 | let panel: DataPanel = [ 217 | "row1": df1, 218 | "row2": df2 219 | ] 220 | 221 | let shape = panel.shape() 222 | 223 | XCTAssertEqual(shape.depth, 2) // 2 top-level keys 224 | XCTAssertEqual(shape.width, 2) // 2 columns in each DataFrame 225 | XCTAssertEqual(shape.height, 3) // 3 rows in each DataFrame 226 | } 227 | 228 | func test_shape_WithEmptyPanel() { 229 | let panel: DataPanel = [:] 230 | 231 | let shape = panel.shape() 232 | 233 | XCTAssertEqual(shape.depth, 0) 234 | XCTAssertEqual(shape.width, 0) 235 | XCTAssertEqual(shape.height, 0) 236 | } 237 | 238 | func test_shape_WithEmptyDataFrames() { 239 | let df1: DataFrame = [:] 240 | let df2: DataFrame = [:] 241 | 242 | let panel: DataPanel = [ 243 | "row1": df1, 244 | "row2": df2 245 | ] 246 | 247 | let shape = panel.shape() 248 | 249 | XCTAssertEqual(shape.depth, 2) 250 | XCTAssertEqual(shape.width, 0) 251 | XCTAssertEqual(shape.height, 0) 252 | } 253 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Koalas Logo 3 |

4 | 5 | [![Swift](https://img.shields.io/badge/Swift-5.2+-orange.svg)](https://swift.org) 6 | [![Platform](https://img.shields.io/badge/platform-macOS%20%7C%20iOS-lightgrey.svg)](https://swift.org) 7 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) 8 | 9 | A powerful Swift library for multidimensional data manipulation, inspired by Python's pandas. Koalas provides native Swift implementations of DataSeries, DataFrame, and DataPanel for efficient data analysis and manipulation. 10 | 11 | **Perfect for iOS/macOS apps that need data analysis capabilities without external dependencies.** 12 | 13 | ## Table of Contents 14 | - [Features](#features) 15 | - [Installation](#installation) 16 | - [Quick Start](#quick-start) 17 | - [Data Structures](#data-structures) 18 | - [Advanced Features](#advanced-features) 19 | - [Requirements](#requirements) 20 | - [License](#license) 21 | 22 | ## Features 23 | 24 | - **Multi-dimensional Data Structures**: DataSeries (1D), DataFrame (2D), DataPanel (3D) 25 | - **Comprehensive Arithmetic Operations**: Memberwise operations with automatic alignment 26 | - **Statistical Functions**: Sum, mean, standard deviation, expanding and rolling windows 27 | - **Missing Data Handling**: Multiple strategies for nil value management 28 | - **Data Manipulation**: Shifting, filling, conditional operations, and reshaping 29 | - **IO Operations**: CSV read/write, JSON encoding/decoding 30 | - **Type Safety**: Full Swift type system integration with generics and protocols 31 | - **Performance**: Built on Swift's collections 32 | 33 | ## Installation 34 | 35 | ### Swift Package Manager 36 | 37 | Add Koalas to your project using Swift Package Manager: 38 | 39 | 1. In Xcode, go to **File** → **Add Package Dependencies** 40 | 2. Enter the repository URL: `https://github.com/your-username/Koalas.git` 41 | 3. Select the version you want to use 42 | 4. Click **Add Package** 43 | 44 | Or add it to your `Package.swift`: 45 | ```swift 46 | dependencies: [ 47 | .package(url: "https://github.com/your-username/Koalas.git", from: "1.0.0") 48 | ] 49 | ``` 50 | 51 | ```swift 52 | import Koalas 53 | ``` 54 | 55 | ## Quick Start 56 | 57 | ### Creating DataFrames 58 | 59 | ```swift 60 | import Koalas 61 | 62 | // Create a DataFrame with multiple columns 63 | let df = DataFrame(dictionaryLiteral: 64 | ("A", DataSeries([1, 2, 3, 4, 5])), 65 | ("B", DataSeries([10, 20, 30, 40, 50])), 66 | ("C", DataSeries([100, 200, 300, 400, 500])) 67 | ) 68 | 69 | // Create a constant DataFrame with the same shape 70 | let constDf = df.mapTo(constant: 10.0) 71 | ``` 72 | 73 | ### Basic Operations 74 | 75 | ```swift 76 | // Arithmetic operations 77 | let sum = df + constDf 78 | let diff = df - constDf 79 | let product = df * constDf 80 | let quotient = df / constDf 81 | 82 | // Statistical operations 83 | let columnSums = df.columnSum() 84 | let means = df.mean() 85 | let stdDevs = df.std() 86 | let sums = df.sum() 87 | 88 | // Expanding operations 89 | let expandingSums = df.expandingSum(initial: 0) 90 | let expandingMax = df.expandingMax() 91 | let expandingMin = df.expandingMin() 92 | ``` 93 | 94 | ### Working with Missing Data 95 | 96 | ```swift 97 | // Create DataFrame with nil values 98 | var dfWithNils = DataFrame(dictionaryLiteral: 99 | ("A", DataSeries([1, nil, 3, nil, 5])), 100 | ("B", DataSeries([10, 20, nil, 40, 50])) 101 | ) 102 | 103 | // Fill nil values 104 | let filledForward = dfWithNils.fillNils(method: .forward(initial: 0)) 105 | let filledBackward = dfWithNils.fillNils(method: .backward(initial: nil)) 106 | let filledConstant = dfWithNils.fillNils(method: .all(0)) 107 | ``` 108 | 109 | ### Time Series Operations 110 | 111 | ```swift 112 | // Shift data (useful for time series) 113 | let shifted = df.shiftedBy(2) // Shift forward by 2 positions 114 | 115 | // Rolling window operations 116 | let rollingSum = df.rollingSum(window: 3) 117 | let rollingMean = df.rollingMean(window: 3) 118 | 119 | // Custom rolling function 120 | let rollingCustom = df.rollingFunc(initial: 0, window: 3) { window in 121 | // Custom aggregation logic 122 | return window.compactMap { $0 }.reduce(0, +) 123 | } 124 | ``` 125 | 126 | ### Conditional Operations 127 | 128 | ```swift 129 | // Create condition DataFrame 130 | let condition = df > 25 131 | 132 | // Apply conditional logic 133 | let result = whereCondition(condition, then: df * 2, else: df / 2) 134 | ``` 135 | 136 | ### Data Import/Export 137 | 138 | ```swift 139 | // Write DataFrame to CSV 140 | try df.write(toFile: "data.csv", columnSeparator: ",") 141 | 142 | // Read DataFrame from CSV 143 | let importedDf = try DataFrame( 144 | contentsOfFile: "data.csv", 145 | columnSeparator: "," 146 | ) 147 | 148 | // Convert to string representation 149 | let csvLines = df.toStringRowLines(separator: ",") 150 | ``` 151 | 152 | ## Usage Examples 153 | 154 | ### Financial Data Analysis 155 | ```swift 156 | // Calculate moving averages for stock prices 157 | let stockData = DataFrame(dictionaryLiteral: 158 | ("price", DataSeries([100.0, 102.0, 98.0, 105.0, 103.0])), 159 | ("volume", DataSeries([1000, 1200, 800, 1500, 1100])) 160 | ) 161 | 162 | let movingAverage = stockData["price"]!.rollingMean(window: 3) 163 | ``` 164 | 165 | ### Data Cleaning 166 | ```swift 167 | // Clean dataset with missing values 168 | let rawData = DataFrame(dictionaryLiteral: 169 | ("name", DataSeries(["Alice", "Bob", nil, "Charlie"])), 170 | ("city", DataSeries(["NYC", nil, "LA", "Chicago"])), 171 | ("status", DataSeries(["active", "inactive", nil, "active"])) 172 | ) 173 | 174 | let cleanedData = rawData.fillNils(method: .forward(initial: "Unknown")) 175 | ``` 176 | 177 | ### Working with Different Data Types 178 | ```swift 179 | // For mixed data types, use separate DataFrames or zipSeries 180 | let names = DataSeries(["Alice", "Bob", "Charlie"]) 181 | let ages = DataSeries([25, 30, 35]) 182 | let scores = DataSeries([85, 92, 88]) 183 | 184 | // Combine different types using zipSeries 185 | let combined = zipSeries(names, ages, scores) 186 | // Returns: DataSeries> 187 | ``` 188 | 189 | ## Data Structures 190 | 191 | ### DataSeries 192 | 193 | A 1-dimensional data structure for handling arrays with optional values: 194 | 195 | ```swift 196 | // Create DataSeries 197 | let series = DataSeries([1, 2, nil, 4, 5]) 198 | 199 | // Basic operations 200 | let doubled = series * 2 201 | let shifted = series.shiftedBy(1) 202 | let filled = series.fillNils(method: .forward(initial: 0)) 203 | 204 | // Statistical functions 205 | let sum = series.sum() 206 | let mean = series.mean() 207 | let std = series.std() 208 | ``` 209 | 210 | ### DataFrame 211 | 212 | A 2-dimensional data structure implemented as a dictionary of DataSeries: 213 | 214 | ```swift 215 | // Create DataFrame 216 | let df = DataFrame(dictionaryLiteral: 217 | ("col1", DataSeries([1, 2, 3])), 218 | ("col2", DataSeries([4, 5, 6])) 219 | ) 220 | 221 | // Access shape - returns tuple 222 | let (width, height) = df.shape() 223 | 224 | // Column operations - these return optional values 225 | let columnSums = df.columnSum() // Returns DataSeries? 226 | let rowSums = df.sum() // Returns DataFrame 227 | ``` 228 | 229 | ### DataPanel 230 | 231 | A 3-dimensional data structure for handling multiple DataFrames: 232 | 233 | ```swift 234 | // Create DataPanel 235 | let panel = DataPanel(dictionaryLiteral: 236 | ("group1", DataFrame(dictionaryLiteral: 237 | ("A", DataSeries([1, 2, 3])), 238 | ("B", DataSeries([4, 5, 6])) 239 | )), 240 | ("group2", DataFrame(dictionaryLiteral: 241 | ("A", DataSeries([7, 8, 9])), 242 | ("B", DataSeries([10, 11, 12])) 243 | )) 244 | ) 245 | 246 | // Transpose panel 247 | let transposed = panel.transposed() 248 | ``` 249 | 250 | ## Advanced Features 251 | 252 | ### Custom Aggregations 253 | 254 | ```swift 255 | // Custom rolling function 256 | let customRolling = df.rollingFunc(initial: 0, window: 3) { window in 257 | // Calculate median of window 258 | let sorted = window.compactMap { $0 }.sorted() 259 | let mid = sorted.count / 2 260 | return sorted.count % 2 == 0 ? 261 | (sorted[mid - 1] + sorted[mid]) / 2 : 262 | sorted[mid] 263 | } 264 | ``` 265 | 266 | ### Data Alignment 267 | 268 | ```swift 269 | // DataFrames are automatically aligned by keys 270 | let df1 = DataFrame(dictionaryLiteral: 271 | ("A", DataSeries([1, 2, 3])), 272 | ("B", DataSeries([4, 5, 6])) 273 | ) 274 | 275 | let df2 = DataFrame(dictionaryLiteral: 276 | ("B", DataSeries([7, 8, 9])), 277 | ("A", DataSeries([10, 11, 12])) 278 | ) 279 | 280 | // Operations automatically align by column names 281 | let result = df1 + df2 282 | ``` 283 | 284 | ### Type Safety 285 | 286 | ```swift 287 | // Strong typing ensures type safety 288 | let intDf = DataFrame(dictionaryLiteral: 289 | ("A", DataSeries([1, 2, 3])) 290 | ) 291 | 292 | let doubleDf = DataFrame(dictionaryLiteral: 293 | ("A", DataSeries([1.0, 2.0, 3.0])) 294 | ) 295 | 296 | // Type-safe operations 297 | let result: DataFrame = intDf + doubleDf //Error 298 | ``` 299 | 300 | ## Requirements 301 | 302 | - **Swift**: 5.2 or later 303 | - **Platforms**: macOS 10.15+, iOS 8.0+ 304 | - **Xcode**: 11.0 or later 305 | 306 | ## License 307 | 308 | Koalas is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. 309 | -------------------------------------------------------------------------------- /Sources/Koalas/DataFrame/DataFrame.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Sergey Kazakov on 04.06.2020. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | A dictionary-like structure that maps keys to DataSeries. 12 | Used to store and manipulate 2D tabular data with multiple columns (DataSeries). 13 | */ 14 | public typealias DataFrame = Dictionary> 15 | 16 | public extension DataFrame { 17 | /** 18 | Initializes a DataFrame with unique keys and their corresponding DataSeries. 19 | Ensures all DataSeries have equal length for proper DataFrame structure. 20 | */ 21 | init(uniqueKeysWithSeries keysAndValues: S) 22 | where 23 | S: Sequence, 24 | S.Element == (Key, DataSeries), 25 | Value == DataSeries { 26 | 27 | let firstDataSeriesCount = keysAndValues.first(where: { _ in true })?.1.count ?? 0 28 | 29 | let allSeriesCountsAreEqual = keysAndValues.allSatisfy { $0.1.count == firstDataSeriesCount } 30 | 31 | assert(allSeriesCountsAreEqual, "DataSeries should have equal length") 32 | self = Dictionary>(uniqueKeysWithValues: keysAndValues) 33 | } 34 | } 35 | 36 | public extension DataFrame { 37 | /** 38 | Transforms each DataSeries into a DataFrame and returns a transposed DataPanel. 39 | Useful for restructuring data from wide to long format. 40 | */ 41 | func upscaleTransform(transform: (DataSeries) -> DataFrame) -> DataPanel where Value == DataSeries { 42 | 43 | let keyValues = map { ($0.key, transform($0.value)) } 44 | let dataPanel = DataPanel(uniqueKeysWithValues: keyValues) 45 | 46 | return dataPanel.transposed() 47 | } 48 | 49 | /** 50 | Applies a transformation function to each value in the DataFrame, handling nil values. 51 | Returns a new DataFrame with transformed values. 52 | */ 53 | func flatMapValues(transform: (V?) -> U?) -> DataFrame where Value == DataSeries { 54 | return mapValues { series in DataSeries(series.map { transform($0) }) } 55 | } 56 | 57 | /** 58 | Maps all values in the DataFrame to a constant value. 59 | Returns a new DataFrame with the same keys but all values replaced by the constant. 60 | */ 61 | func mapTo(constant value: Constant) -> DataFrame where Value == DataSeries { 62 | return mapValues { $0.mapTo(constant: value) } 63 | } 64 | 65 | /** 66 | Maps all values in the DataFrame to a single DataSeries. 67 | Returns nil if the provided series is nil or has different length than existing series. 68 | */ 69 | func mapTo(series value: DataSeries?) -> DataFrame? where Value == DataSeries { 70 | guard let value = value else { 71 | return nil 72 | } 73 | 74 | return mapValues { 75 | assert($0.count == value.count, "DataSeries should have equal length") 76 | return value 77 | } 78 | } 79 | 80 | /** 81 | Applies a scan operation to each DataSeries in the DataFrame. 82 | Performs cumulative operations with an initial value and transformation function. 83 | */ 84 | func scan(initial: T?, _ nextPartialResult: (T?, V?) -> T?) -> DataFrame where Value == DataSeries, V: Numeric { 85 | return mapValues { $0.scanSeries(initial: initial, nextPartialResult) } 86 | } 87 | 88 | /** 89 | Shifts all DataSeries in the DataFrame by the specified amount. 90 | Positive values shift forward, negative values shift backward. 91 | */ 92 | func shiftedBy(_ amount: Int) -> DataFrame where Value == DataSeries { 93 | return mapValues { $0.shiftedBy(amount) } 94 | } 95 | 96 | /** 97 | Calculates expanding sum for each DataSeries in the DataFrame. 98 | Returns cumulative sums starting from the initial value. 99 | */ 100 | func expandingSum(initial: V) -> DataFrame where Value == DataSeries, V: Numeric { 101 | return mapValues { $0.expandingSum(initial: initial) } 102 | } 103 | 104 | /** 105 | Calculates expanding maximum for each DataSeries in the DataFrame. 106 | Returns cumulative maximum values. 107 | */ 108 | func expandingMax() -> DataFrame where Value == DataSeries, V: Comparable { 109 | return mapValues { $0.expandingMax() } 110 | } 111 | 112 | /** 113 | Calculates expanding minimum for each DataSeries in the DataFrame. 114 | Returns cumulative minimum values. 115 | */ 116 | func expandingMin() -> DataFrame where Value == DataSeries, V: Comparable { 117 | return mapValues { $0.expandingMin() } 118 | } 119 | 120 | /** 121 | Applies a rolling window function to each DataSeries in the DataFrame. 122 | Uses a custom window function to process values within the specified window size. 123 | */ 124 | func rollingFunc(initial: V, window: Int, windowFunc: (([V?]) -> V?)) -> DataFrame where Value == DataSeries, V: Numeric { 125 | return mapValues { $0.rollingFunc(initial: initial, window: window, windowFunc: windowFunc)} 126 | } 127 | 128 | /** 129 | Calculates rolling sum for each DataSeries in the DataFrame. 130 | Uses the specified window size for the rolling calculation. 131 | */ 132 | func rollingSum(window: Int) -> DataFrame where Value == DataSeries, V: Numeric { 133 | return mapValues { $0.rollingSum(window: window) } 134 | } 135 | 136 | /** 137 | Calculates rolling mean for each DataSeries in the DataFrame. 138 | Uses the specified window size for the rolling calculation. 139 | */ 140 | func rollingMean(window: Int) -> DataFrame where Value == DataSeries, V: FloatingPoint { 141 | return mapValues { $0.rollingMean(window: window) } 142 | } 143 | 144 | /** 145 | Compares this DataFrame with another DataFrame for equality. 146 | Returns true if both DataFrames have the same keys and corresponding DataSeries are equal. 147 | */ 148 | func equalsTo(dataframe: DataFrame?) -> Bool where Value == DataSeries, V: Equatable { 149 | guard let dataframe = dataframe else { 150 | return false 151 | } 152 | 153 | guard Set(self.keys) == Set(dataframe.keys) else { 154 | return false 155 | } 156 | 157 | return self.first { !$0.value.equalsTo(series: dataframe[$0.key]) } == nil 158 | } 159 | 160 | /** 161 | Compares this DataFrame with another DataFrame for equality with precision tolerance. 162 | Useful for floating-point comparisons where exact equality is not required. 163 | */ 164 | func equalsTo(dataframe: DataFrame?, with precision: V) -> Bool where Value == DataSeries, V: FloatingPoint { 165 | guard let dataframe = dataframe else { 166 | return false 167 | } 168 | 169 | guard Set(self.keys) == Set(dataframe.keys) else { 170 | return false 171 | } 172 | 173 | return self.first { !$0.value.equalsTo(series: dataframe[$0.key], with: precision) } == nil 174 | } 175 | } 176 | 177 | /** 178 | Applies a conditional operation to DataFrames based on a boolean condition. 179 | Returns the true DataFrame where condition is true, false DataFrame where condition is false. 180 | */ 181 | public func whereCondition(_ condition: DataFrame?, 182 | then trueDF: DataFrameType, T>, 183 | else df: DataFrameType, T>) -> DataFrame? { 184 | guard let condition = condition else { 185 | return nil 186 | } 187 | 188 | guard let trueDF = trueDF.toDataframeWithShape(of: condition) else { 189 | return nil 190 | } 191 | 192 | guard let falseDF = df.toDataframeWithShape(of: condition) else { 193 | return nil 194 | } 195 | 196 | return whereCondition(condition, then: trueDF, else: falseDF) 197 | } 198 | 199 | /** 200 | Applies a conditional operation to DataFrames based on a boolean condition. 201 | Returns the true DataFrame where condition is true, false DataFrame where condition is false. 202 | */ 203 | public func whereCondition(_ condition: DataFrame?, 204 | then trueDataFrame: DataFrame?, 205 | else dataframe: DataFrame?) -> DataFrame? { 206 | 207 | guard let condition = condition, 208 | let trueDataFrame = trueDataFrame, 209 | let dataframe = dataframe 210 | else { 211 | return nil 212 | } 213 | 214 | let keysSet = Set(condition.keys) 215 | assert(keysSet == Set(trueDataFrame.keys), "Dataframes should have equal keys sets") 216 | assert(keysSet == Set(dataframe.keys), "Dataframes should have equal keys sets") 217 | 218 | var res = DataFrame() 219 | 220 | keysSet.forEach { key in 221 | res[key] = unwrap(condition[key], 222 | trueDataFrame[key], 223 | dataframe[key]) { return whereCondition($0, then: $1, else: $2) } 224 | } 225 | 226 | return res 227 | } 228 | 229 | public extension DataFrame { 230 | /** 231 | Returns the dimensions of the DataFrame as (width, height). 232 | Width is the number of columns (keys), height is the length of DataSeries. 233 | */ 234 | func shape() -> (width: Int, height: Int) where Value == DataSeries { 235 | return (self.keys.count, self.values.first?.count ?? 0) 236 | } 237 | 238 | /** 239 | Calculates the sum of each DataSeries in the DataFrame. 240 | Returns a DataFrame with single-value DataSeries containing the sums. 241 | */ 242 | func sum(ignoreNils: Bool = true) -> DataFrame where Value == DataSeries, V: Numeric { 243 | mapValues { DataSeries([$0.sum(ignoreNils: ignoreNils)]) } 244 | } 245 | 246 | /** 247 | Calculates the sum of all columns (DataSeries) in the DataFrame. 248 | Returns a single DataSeries with the sum of corresponding elements across all columns. 249 | */ 250 | func columnSum(ignoreNils: Bool = true) -> DataSeries? where Value == DataSeries, V: Numeric { 251 | guard let first = values.first else { 252 | return nil 253 | } 254 | 255 | let initial = DataSeries(repeating: 0, count: first.count) 256 | 257 | return values.reduce(initial) { (currentRes: DataSeries, next: DataSeries) -> DataSeries in 258 | let nextSeries: DataSeries = ignoreNils ? next.fillNils(with: 0) : next 259 | return currentRes + nextSeries 260 | } 261 | } 262 | 263 | /** 264 | Calculates the mean of each DataSeries in the DataFrame. 265 | Returns a DataFrame with single-value DataSeries containing the means. 266 | */ 267 | func mean(shouldSkipNils: Bool = true) -> DataFrame where Value == DataSeries, V: FloatingPoint { 268 | mapValues { DataSeries([$0.mean(shouldSkipNils: shouldSkipNils)]) } 269 | } 270 | 271 | /** 272 | Calculates the standard deviation of each DataSeries in the DataFrame. 273 | Returns a DataFrame with single-value DataSeries containing the standard deviations. 274 | */ 275 | func std(shouldSkipNils: Bool = true) -> DataFrame where Value == DataSeries, V: FloatingPoint { 276 | mapValues { DataSeries([$0.std(shouldSkipNils: shouldSkipNils)]) } 277 | } 278 | 279 | /** 280 | Fills nil values in all DataSeries of the DataFrame using the specified method. 281 | Returns a new DataFrame with nil values replaced according to the fill method. 282 | */ 283 | func fillNils(method: FillNilsMethod) -> DataFrame where Value == DataSeries { 284 | mapValues { $0.fillNils(method: method) } 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /Tests/KoalasTests/DataSeriesAdvancedTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataSeriesAdvancedTests.swift 3 | // 4 | // 5 | // Created by AI Assistant on 2024. 6 | // 7 | 8 | import XCTest 9 | @testable import Koalas 10 | 11 | final class DataSeriesAdvancedTests: XCTestCase { 12 | 13 | // MARK: - whereCondition Tests 14 | 15 | func test_whereCondition_WithDataSeriesType() { 16 | let condition = DataSeries([true, false, true, false]) 17 | let trueSeries = DataSeries([1, 2, 3, 4]) 18 | let falseSeries = DataSeries([10, 20, 30, 40]) 19 | 20 | let result = whereCondition(condition, 21 | then: .ds(trueSeries), 22 | else: .ds(falseSeries)) 23 | 24 | XCTAssertNotNil(result) 25 | XCTAssertEqual(result?[0], 1) // true -> trueSeries 26 | XCTAssertEqual(result?[1], 20) // false -> falseSeries 27 | XCTAssertEqual(result?[2], 3) // true -> trueSeries 28 | XCTAssertEqual(result?[3], 40) // false -> falseSeries 29 | } 30 | 31 | func test_whereCondition_WithScalarValues() { 32 | let condition = DataSeries([true, false, true, false]) 33 | let trueValue = 100 34 | let falseValue = 200 35 | 36 | let result = whereCondition(condition, 37 | then: trueValue, 38 | else: falseValue) 39 | 40 | XCTAssertNotNil(result) 41 | XCTAssertEqual(result?[0], 100) // true -> trueValue 42 | XCTAssertEqual(result?[1], 200) // false -> falseValue 43 | XCTAssertEqual(result?[2], 100) // true -> trueValue 44 | XCTAssertEqual(result?[3], 200) // false -> falseValue 45 | } 46 | 47 | func test_whereCondition_WithNilCondition() { 48 | let trueSeries = DataSeries([1, 2, 3, 4]) 49 | let falseSeries = DataSeries([10, 20, 30, 40]) 50 | 51 | let result = whereCondition(nil, 52 | then: .ds(trueSeries), 53 | else: .ds(falseSeries)) 54 | 55 | XCTAssertNil(result) 56 | } 57 | 58 | func test_whereCondition_WithNilSeries() { 59 | let condition = DataSeries([true, false, true, false]) 60 | 61 | let result: DataSeries? = whereCondition(condition, 62 | then: nil, 63 | else: nil) 64 | 65 | XCTAssertNil(result) 66 | } 67 | 68 | // MARK: - zipSeries Tests 69 | 70 | func test_zipSeries_TwoSeries() { 71 | let s1 = DataSeries([1, 2, 3, 4]) 72 | let s2 = DataSeries([5, 6, 7, 8]) 73 | 74 | let result = zipSeries(s1, s2) 75 | 76 | XCTAssertNotNil(result) 77 | XCTAssertEqual(result?.count, 4) 78 | XCTAssertEqual(result?[0]?.t1, 1) 79 | XCTAssertEqual(result?[0]?.t2, 5) 80 | XCTAssertEqual(result?[1]?.t1, 2) 81 | XCTAssertEqual(result?[1]?.t2, 6) 82 | XCTAssertEqual(result?[2]?.t1, 3) 83 | XCTAssertEqual(result?[2]?.t2, 7) 84 | XCTAssertEqual(result?[3]?.t1, 4) 85 | XCTAssertEqual(result?[3]?.t2, 8) 86 | } 87 | 88 | func test_zipSeries_ThreeSeries() { 89 | let s1 = DataSeries([1, 2, 3, 4]) 90 | let s2 = DataSeries([5, 6, 7, 8]) 91 | let s3 = DataSeries([9, 10, 11, 12]) 92 | 93 | let result = zipSeries(s1, s2, s3) 94 | 95 | XCTAssertNotNil(result) 96 | XCTAssertEqual(result?.count, 4) 97 | XCTAssertEqual(result?[0]?.t1, 1) 98 | XCTAssertEqual(result?[0]?.t2, 5) 99 | XCTAssertEqual(result?[0]?.t3, 9) 100 | XCTAssertEqual(result?[1]?.t1, 2) 101 | XCTAssertEqual(result?[1]?.t2, 6) 102 | XCTAssertEqual(result?[1]?.t3, 10) 103 | } 104 | 105 | func test_zipSeries_FourSeries() { 106 | let s1 = DataSeries([1, 2, 3, 4]) 107 | let s2 = DataSeries([5, 6, 7, 8]) 108 | let s3 = DataSeries([9, 10, 11, 12]) 109 | let s4 = DataSeries([13, 14, 15, 16]) 110 | 111 | let result = zipSeries(s1, s2, s3, s4) 112 | 113 | XCTAssertNotNil(result) 114 | XCTAssertEqual(result?.count, 4) 115 | XCTAssertEqual(result?[0]?.t1, 1) 116 | XCTAssertEqual(result?[0]?.t2, 5) 117 | XCTAssertEqual(result?[0]?.t3, 9) 118 | XCTAssertEqual(result?[0]?.t4, 13) 119 | XCTAssertEqual(result?[1]?.t1, 2) 120 | XCTAssertEqual(result?[1]?.t2, 6) 121 | XCTAssertEqual(result?[1]?.t3, 10) 122 | XCTAssertEqual(result?[1]?.t4, 14) 123 | } 124 | 125 | func test_zipSeries_WithNilSeries() { 126 | let s1 = DataSeries([1, 2, 3, 4]) 127 | 128 | let result: DataSeries>? = zipSeries(s1, nil) 129 | 130 | XCTAssertNil(result) 131 | } 132 | 133 | // func test_zipSeries_WithDifferentLengths() { 134 | // let s1 = DataSeries([1, 2, 3, 4]) 135 | // let s2 = DataSeries([5, 6, 7]) // Different length 136 | // 137 | // let result = zipSeries(s1, s2) 138 | // 139 | // // Should assert and fail in debug mode 140 | // XCTAssertNotNil(result) 141 | // } 142 | 143 | // MARK: - whereTrue Tests 144 | 145 | func test_whereTrue_WithValidSeries() { 146 | let condition = DataSeries([true, false, true, false, nil]) 147 | let trueSeries = DataSeries([1, 2, 3, 4, 5]) 148 | let falseSeries = DataSeries([10, 20, 30, 40, 50]) 149 | 150 | let result = condition.whereTrue(then: trueSeries, else: falseSeries) 151 | 152 | XCTAssertNotNil(result) 153 | XCTAssertEqual(result?[0], 1) // true -> trueSeries 154 | XCTAssertEqual(result?[1], 20) // false -> falseSeries 155 | XCTAssertEqual(result?[2], 3) // true -> trueSeries 156 | XCTAssertEqual(result?[3], 40) // false -> falseSeries 157 | XCTAssertNil(result?[4]) // nil -> nil 158 | } 159 | 160 | func test_whereTrue_WithNilSeries() { 161 | let condition = DataSeries([true, false, true]) 162 | 163 | let result: DataSeries? = condition.whereTrue(then: nil, else: nil) 164 | 165 | XCTAssertNil(result) 166 | } 167 | 168 | // func test_whereTrue_WithDifferentLengths() { 169 | // let condition = DataSeries([true, false, true]) 170 | // let trueSeries = DataSeries([1, 2]) // Shorter 171 | // let falseSeries = DataSeries([10, 20, 30]) 172 | // 173 | // let result = condition.whereTrue(then: trueSeries, else: falseSeries) 174 | // 175 | // XCTAssertNotNil(result) 176 | // XCTAssertEqual(result?[0], 1) // true -> trueSeries 177 | // XCTAssertEqual(result?[1], 20) // false -> falseSeries 178 | // XCTAssertEqual(result?[2], 30) // true -> falseSeries (trueSeries too short) 179 | // } 180 | 181 | // MARK: - isEmptySeries Tests 182 | 183 | func test_isEmptySeries_WithAllNils() { 184 | let series: DataSeries = DataSeries([nil, nil, nil]) 185 | 186 | XCTAssertTrue(series.isEmptySeries()) 187 | } 188 | 189 | func test_isEmptySeries_WithSomeNils() { 190 | let series = DataSeries([1, nil, 3]) 191 | 192 | XCTAssertFalse(series.isEmptySeries()) 193 | } 194 | 195 | func test_isEmptySeries_WithNoNils() { 196 | let series = DataSeries([1, 2, 3]) 197 | 198 | XCTAssertFalse(series.isEmptySeries()) 199 | } 200 | 201 | func test_isEmptySeries_WithEmptySeries() { 202 | let series: DataSeries = DataSeries() 203 | 204 | XCTAssertTrue(series.isEmptySeries()) 205 | } 206 | 207 | // MARK: - at and setAt Tests 208 | 209 | func test_at_WithValidIndex() { 210 | let series = DataSeries([1, 2, 3, 4, 5]) 211 | 212 | XCTAssertEqual(series.at(index: 0), 1) 213 | XCTAssertEqual(series.at(index: 2), 3) 214 | XCTAssertEqual(series.at(index: 4), 5) 215 | } 216 | 217 | func test_at_WithInvalidIndex() { 218 | let series = DataSeries([1, 2, 3, 4, 5]) 219 | 220 | XCTAssertNil(series.at(index: -1) as Any?) 221 | XCTAssertNil(series.at(index: 5) as Any?) 222 | XCTAssertNil(series.at(index: 10) as Any?) 223 | } 224 | 225 | func test_setAt_WithValidIndex() { 226 | let series = DataSeries([1, 2, 3, 4, 5]) 227 | 228 | let result = series.setAt(index: 2, value: 100) 229 | 230 | XCTAssertEqual(result[0], 1) 231 | XCTAssertEqual(result[1], 2) 232 | XCTAssertEqual(result[2], 100) 233 | XCTAssertEqual(result[3], 4) 234 | XCTAssertEqual(result[4], 5) 235 | } 236 | 237 | func test_setAt_WithInvalidIndex() { 238 | let series = DataSeries([1, 2, 3, 4, 5]) 239 | 240 | let result = series.setAt(index: 10, value: 100) 241 | 242 | // Should return unchanged series 243 | XCTAssertEqual(result[0], 1) 244 | XCTAssertEqual(result[1], 2) 245 | XCTAssertEqual(result[2], 3) 246 | XCTAssertEqual(result[3], 4) 247 | XCTAssertEqual(result[4], 5) 248 | } 249 | 250 | // MARK: - scanSeries Tests 251 | 252 | func test_scanSeries_WithAddition() { 253 | let series = DataSeries([1, 2, 3, 4, 5]) 254 | 255 | let result = series.scanSeries(initial: 0) { current, next in 256 | (current ?? 0) + (next ?? 0) 257 | } 258 | 259 | XCTAssertEqual(result.count, 5) 260 | XCTAssertEqual(result[0], 1) 261 | XCTAssertEqual(result[1], 3) 262 | XCTAssertEqual(result[2], 6) 263 | XCTAssertEqual(result[3], 10) 264 | XCTAssertEqual(result[4], 15) 265 | } 266 | 267 | func test_scanSeries_WithNilValues() { 268 | let series = DataSeries([1, nil, 3, 4, 5]) 269 | 270 | let result = series.scanSeries(initial: 0) { current, next in 271 | (current ?? 0) + (next ?? 0) 272 | } 273 | 274 | XCTAssertEqual(result.count, 5) 275 | XCTAssertEqual(result[0], 1) 276 | XCTAssertEqual(result[1], 1) // 1 + 0 (nil) 277 | XCTAssertEqual(result[2], 4) // 1 + 3 278 | XCTAssertEqual(result[3], 8) // 4 + 4 279 | XCTAssertEqual(result[4], 13) // 8 + 5 280 | } 281 | 282 | // MARK: - toDateComponents Tests 283 | 284 | func test_toDateComponents_WithValidDates() { 285 | let dateFormatter = DateFormatter() 286 | dateFormatter.dateFormat = "yyyy/MM/dd" 287 | 288 | let dates = DataSeries([ 289 | dateFormatter.date(from: "2020/01/15"), 290 | dateFormatter.date(from: "2021/06/20"), 291 | dateFormatter.date(from: "2022/12/31") 292 | ]) 293 | 294 | let result = dates.toDateComponents() 295 | 296 | XCTAssertEqual(result[.year]?.count, 3) 297 | XCTAssertEqual(result[.month]?.count, 3) 298 | XCTAssertEqual(result[.day]?.count, 3) 299 | 300 | XCTAssertEqual(result[.year]?[0], 2020) 301 | XCTAssertEqual(result[.month]?[0], 1) 302 | XCTAssertEqual(result[.day]?[0], 15) 303 | 304 | XCTAssertEqual(result[.year]?[1], 2021) 305 | XCTAssertEqual(result[.month]?[1], 6) 306 | XCTAssertEqual(result[.day]?[1], 20) 307 | 308 | XCTAssertEqual(result[.year]?[2], 2022) 309 | XCTAssertEqual(result[.month]?[2], 12) 310 | XCTAssertEqual(result[.day]?[2], 31) 311 | } 312 | 313 | func test_toDateComponents_WithNilDates() { 314 | let dateFormatter = DateFormatter() 315 | dateFormatter.dateFormat = "yyyy/MM/dd" 316 | 317 | let dates = DataSeries([ 318 | dateFormatter.date(from: "2020/01/15"), 319 | nil, 320 | dateFormatter.date(from: "2022/12/31") 321 | ]) 322 | 323 | let result = dates.toDateComponents() 324 | 325 | XCTAssertEqual(result[.year]?.count, 3) 326 | XCTAssertEqual(result[.month]?.count, 3) 327 | XCTAssertEqual(result[.day]?.count, 3) 328 | 329 | XCTAssertEqual(result[.year]?[0], 2020) 330 | XCTAssertNil(result[.year]?[1]) 331 | XCTAssertEqual(result[.year]?[2], 2022) 332 | } 333 | } -------------------------------------------------------------------------------- /Tests/KoalasTests/DataFrameIOTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataFrameIOTests.swift 3 | // 4 | // 5 | // Created by AI Assistant on 2024. 6 | // 7 | 8 | import XCTest 9 | @testable import Koalas 10 | 11 | final class DataFrameIOTests: XCTestCase { 12 | 13 | // MARK: - toStringRowLines Tests 14 | 15 | func test_toStringRowLines_WithValidData() { 16 | let s1 = DataSeries([1, 2, 3, 4]) 17 | let s2 = DataSeries([5, 6, 7, 8]) 18 | 19 | let df = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 20 | 21 | let result = df.toStringRowLines(separator: ",") 22 | 23 | XCTAssertEqual(result.count, 5) // Header + 4 data rows 24 | XCTAssertEqual(result[0], "col1,col2") // Header 25 | XCTAssertEqual(result[1], "1,5") // First row 26 | XCTAssertEqual(result[2], "2,6") // Second row 27 | XCTAssertEqual(result[3], "3,7") // Third row 28 | XCTAssertEqual(result[4], "4,8") // Fourth row 29 | } 30 | 31 | func test_toStringRowLines_WithNilValues() { 32 | let s1 = DataSeries([1, nil, 3, 4]) 33 | let s2 = DataSeries([5, 6, nil, 8]) 34 | 35 | let df = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 36 | 37 | let result = df.toStringRowLines(separator: ";") 38 | 39 | XCTAssertEqual(result.count, 5) // Header + 4 data rows 40 | XCTAssertEqual(result[0], "col1;col2") // Header 41 | XCTAssertEqual(result[1], "1;5") // First row 42 | XCTAssertEqual(result[2], "nil;6") // Second row 43 | XCTAssertEqual(result[3], "3;nil") // Third row 44 | XCTAssertEqual(result[4], "4;8") // Fourth row 45 | } 46 | 47 | func test_toStringRowLines_WithDifferentSeparator() { 48 | let s1 = DataSeries([1, 2, 3]) 49 | let s2 = DataSeries([4, 5, 6]) 50 | 51 | let df = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 52 | 53 | let result = df.toStringRowLines(separator: "|") 54 | 55 | XCTAssertEqual(result.count, 4) // Header + 3 data rows 56 | XCTAssertEqual(result[0], "col1|col2") // Header 57 | XCTAssertEqual(result[1], "1|4") // First row 58 | XCTAssertEqual(result[2], "2|5") // Second row 59 | XCTAssertEqual(result[3], "3|6") // Third row 60 | } 61 | 62 | func test_toStringRowLines_WithEmptyDataFrame() { 63 | let df: DataFrame = [:] 64 | 65 | let result = df.toStringRowLines(separator: ",") 66 | 67 | XCTAssertEqual(result.count, 1) // Only header 68 | XCTAssertEqual(result[0], "") // Empty header 69 | } 70 | 71 | // MARK: - write Tests 72 | 73 | func test_write_WithValidData() throws { 74 | let s1 = DataSeries([1, 2, 3, 4]) 75 | let s2 = DataSeries([5, 6, 7, 8]) 76 | 77 | let df = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 78 | 79 | let fileManager = FileManager.default 80 | let fileURL = fileManager.temporaryDirectory.appendingPathComponent("test_write").appendingPathExtension("csv") 81 | 82 | try df.write(toFile: fileURL.path, columnSeparator: ",") 83 | 84 | // Verify file was created 85 | XCTAssertTrue(fileManager.fileExists(atPath: fileURL.path)) 86 | 87 | // Read and verify content 88 | let content = try String(contentsOf: fileURL, encoding: .utf8) 89 | let lines = content.components(separatedBy: .newlines).filter { !$0.isEmpty } 90 | 91 | XCTAssertEqual(lines.count, 5) // Header + 4 data rows 92 | XCTAssertEqual(lines[0], "col1,col2") 93 | XCTAssertEqual(lines[1], "1,5") 94 | XCTAssertEqual(lines[2], "2,6") 95 | XCTAssertEqual(lines[3], "3,7") 96 | XCTAssertEqual(lines[4], "4,8") 97 | 98 | // Clean up 99 | try fileManager.removeItem(at: fileURL) 100 | } 101 | 102 | func test_write_WithNilValues() throws { 103 | let s1 = DataSeries([1, nil, 3, 4]) 104 | let s2 = DataSeries([5, 6, nil, 8]) 105 | 106 | let df = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 107 | 108 | let fileManager = FileManager.default 109 | let fileURL = fileManager.temporaryDirectory.appendingPathComponent("test_write_nil").appendingPathExtension("csv") 110 | 111 | try df.write(toFile: fileURL.path, columnSeparator: ";") 112 | 113 | // Verify file was created 114 | XCTAssertTrue(fileManager.fileExists(atPath: fileURL.path)) 115 | 116 | // Read and verify content 117 | let content = try String(contentsOf: fileURL, encoding: .utf8) 118 | let lines = content.components(separatedBy: .newlines).filter { !$0.isEmpty } 119 | 120 | XCTAssertEqual(lines.count, 5) // Header + 4 data rows 121 | XCTAssertEqual(lines[0], "col1;col2") 122 | XCTAssertEqual(lines[1], "1;5") 123 | XCTAssertEqual(lines[2], "nil;6") 124 | XCTAssertEqual(lines[3], "3;nil") 125 | XCTAssertEqual(lines[4], "4;8") 126 | 127 | // Clean up 128 | try fileManager.removeItem(at: fileURL) 129 | } 130 | 131 | func test_write_WithCustomEncoding() throws { 132 | let s1 = DataSeries([1, 2, 3]) 133 | let s2 = DataSeries([4, 5, 6]) 134 | 135 | let df = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 136 | 137 | let fileManager = FileManager.default 138 | let fileURL = fileManager.temporaryDirectory.appendingPathComponent("test_write_encoding").appendingPathExtension("csv") 139 | 140 | try df.write(toFile: fileURL.path, encoding: .utf8, columnSeparator: "|") 141 | 142 | // Verify file was created 143 | XCTAssertTrue(fileManager.fileExists(atPath: fileURL.path)) 144 | 145 | // Read and verify content 146 | let content = try String(contentsOf: fileURL, encoding: .utf8) 147 | let lines = content.components(separatedBy: .newlines).filter { !$0.isEmpty } 148 | 149 | XCTAssertEqual(lines.count, 4) // Header + 3 data rows 150 | XCTAssertEqual(lines[0], "col1|col2") 151 | XCTAssertEqual(lines[1], "1|4") 152 | XCTAssertEqual(lines[2], "2|5") 153 | XCTAssertEqual(lines[3], "3|6") 154 | 155 | // Clean up 156 | try fileManager.removeItem(at: fileURL) 157 | } 158 | 159 | // MARK: - init from file Tests 160 | 161 | func test_initFromFile_WithValidData() throws { 162 | let fileManager = FileManager.default 163 | let fileURL = fileManager.temporaryDirectory.appendingPathComponent("test_init").appendingPathExtension("csv") 164 | 165 | // Create test file 166 | let content = """ 167 | col1,col2,col3 168 | 1,5,9 169 | 2,6,10 170 | 3,7,11 171 | 4,8,12 172 | """ 173 | try content.write(to: fileURL, atomically: true, encoding: .utf8) 174 | 175 | let df = try DataFrame(contentsOfFile: fileURL.path, columnSeparator: ",") 176 | 177 | XCTAssertEqual(df.count, 3) // 3 columns 178 | XCTAssertEqual(df["col1"]?.count, 4) // 4 rows 179 | XCTAssertEqual(df["col2"]?.count, 4) 180 | XCTAssertEqual(df["col3"]?.count, 4) 181 | 182 | XCTAssertEqual(df["col1"]?[0], 1) 183 | XCTAssertEqual(df["col1"]?[1], 2) 184 | XCTAssertEqual(df["col1"]?[2], 3) 185 | XCTAssertEqual(df["col1"]?[3], 4) 186 | 187 | XCTAssertEqual(df["col2"]?[0], 5) 188 | XCTAssertEqual(df["col2"]?[1], 6) 189 | XCTAssertEqual(df["col2"]?[2], 7) 190 | XCTAssertEqual(df["col2"]?[3], 8) 191 | 192 | XCTAssertEqual(df["col3"]?[0], 9) 193 | XCTAssertEqual(df["col3"]?[1], 10) 194 | XCTAssertEqual(df["col3"]?[2], 11) 195 | XCTAssertEqual(df["col3"]?[3], 12) 196 | 197 | // Clean up 198 | try fileManager.removeItem(at: fileURL) 199 | } 200 | 201 | func test_initFromFile_WithNilValues() throws { 202 | let fileManager = FileManager.default 203 | let fileURL = fileManager.temporaryDirectory.appendingPathComponent("test_init_nil").appendingPathExtension("csv") 204 | 205 | // Create test file with nil values 206 | let content = """ 207 | col1,col2,col3 208 | 1,5,9 209 | nil,6,10 210 | 3,nil,11 211 | 4,8,nil 212 | """ 213 | try content.write(to: fileURL, atomically: true, encoding: .utf8) 214 | 215 | let df = try DataFrame(contentsOfFile: fileURL.path, columnSeparator: ",") 216 | 217 | XCTAssertEqual(df.count, 3) // 3 columns 218 | XCTAssertEqual(df["col1"]?.count, 4) // 4 rows 219 | 220 | XCTAssertEqual(df["col1"]?[0], 1) 221 | XCTAssertNil(df["col1"]?[1]) 222 | XCTAssertEqual(df["col1"]?[2], 3) 223 | XCTAssertEqual(df["col1"]?[3], 4) 224 | 225 | XCTAssertEqual(df["col2"]?[0], 5) 226 | XCTAssertEqual(df["col2"]?[1], 6) 227 | XCTAssertNil(df["col2"]?[2]) 228 | XCTAssertEqual(df["col2"]?[3], 8) 229 | 230 | XCTAssertEqual(df["col3"]?[0], 9) 231 | XCTAssertEqual(df["col3"]?[1], 10) 232 | XCTAssertEqual(df["col3"]?[2], 11) 233 | XCTAssertNil(df["col3"]?[3]) 234 | 235 | // Clean up 236 | try fileManager.removeItem(at: fileURL) 237 | } 238 | 239 | func test_initFromFile_WithDifferentSeparator() throws { 240 | let fileManager = FileManager.default 241 | let fileURL = fileManager.temporaryDirectory.appendingPathComponent("test_init_separator").appendingPathExtension("csv") 242 | 243 | // Create test file with different separator 244 | let content = """ 245 | col1|col2 246 | 1|5 247 | 2|6 248 | 3|7 249 | """ 250 | try content.write(to: fileURL, atomically: true, encoding: .utf8) 251 | 252 | let df = try DataFrame(contentsOfFile: fileURL.path, columnSeparator: "|") 253 | 254 | XCTAssertEqual(df.count, 2) // 2 columns 255 | XCTAssertEqual(df["col1"]?.count, 3) // 3 rows 256 | XCTAssertEqual(df["col2"]?.count, 3) 257 | 258 | XCTAssertEqual(df["col1"]?[0], 1) 259 | XCTAssertEqual(df["col1"]?[1], 2) 260 | XCTAssertEqual(df["col1"]?[2], 3) 261 | 262 | XCTAssertEqual(df["col2"]?[0], 5) 263 | XCTAssertEqual(df["col2"]?[1], 6) 264 | XCTAssertEqual(df["col2"]?[2], 7) 265 | 266 | // Clean up 267 | try fileManager.removeItem(at: fileURL) 268 | } 269 | 270 | func test_initFromFile_WithEmptyFile() throws { 271 | let fileManager = FileManager.default 272 | let fileURL = fileManager.temporaryDirectory.appendingPathComponent("test_init_empty").appendingPathExtension("csv") 273 | 274 | // Create empty file 275 | try "".write(to: fileURL, atomically: true, encoding: .utf8) 276 | 277 | let df = try DataFrame(contentsOfFile: fileURL.path, columnSeparator: ",") 278 | 279 | XCTAssertEqual(df.count, 0) // No columns 280 | 281 | // Clean up 282 | try fileManager.removeItem(at: fileURL) 283 | } 284 | 285 | func test_initFromFile_WithOnlyHeader() throws { 286 | let fileManager = FileManager.default 287 | let fileURL = fileManager.temporaryDirectory.appendingPathComponent("test_init_header").appendingPathExtension("csv") 288 | 289 | // Create file with only header 290 | let content = "col1,col2,col3" 291 | try content.write(to: fileURL, atomically: true, encoding: .utf8) 292 | 293 | let df = try DataFrame(contentsOfFile: fileURL.path, columnSeparator: ",") 294 | 295 | XCTAssertEqual(df.count, 3) // 3 columns 296 | XCTAssertEqual(df["col1"]?.count, 0) // No data rows 297 | XCTAssertEqual(df["col2"]?.count, 0) 298 | XCTAssertEqual(df["col3"]?.count, 0) 299 | 300 | // Clean up 301 | try fileManager.removeItem(at: fileURL) 302 | } 303 | 304 | func test_initFromFile_WithInvalidPath() { 305 | let invalidPath = "/nonexistent/path/file.csv" 306 | 307 | XCTAssertThrowsError(try DataFrame(contentsOfFile: invalidPath, columnSeparator: ",")) 308 | } 309 | 310 | // MARK: - Round-trip Tests 311 | 312 | func test_writeAndRead_RoundTrip() throws { 313 | let s1 = DataSeries([1, 2, 3, 4]) 314 | let s2 = DataSeries([5, 6, 7, 8]) 315 | let s3 = DataSeries([9, 10, 11, 12]) 316 | 317 | let originalDF = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2), ("col3", s3)) 318 | 319 | let fileManager = FileManager.default 320 | let fileURL = fileManager.temporaryDirectory.appendingPathComponent("test_roundtrip").appendingPathExtension("csv") 321 | 322 | // Write DataFrame 323 | try originalDF.write(toFile: fileURL.path, columnSeparator: ",") 324 | 325 | // Read DataFrame back 326 | let readDF = try DataFrame(contentsOfFile: fileURL.path, columnSeparator: ",") 327 | 328 | // Verify they are equal 329 | XCTAssertTrue(originalDF.equalsTo(dataframe: readDF)) 330 | 331 | // Clean up 332 | try fileManager.removeItem(at: fileURL) 333 | } 334 | 335 | func test_writeAndRead_RoundTripWithNilValues() throws { 336 | let s1 = DataSeries([1, nil, 3, 4]) 337 | let s2 = DataSeries([5, 6, nil, 8]) 338 | let s3 = DataSeries([9, 10, 11, nil]) 339 | 340 | let originalDF = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2), ("col3", s3)) 341 | 342 | let fileManager = FileManager.default 343 | let fileURL = fileManager.temporaryDirectory.appendingPathComponent("test_roundtrip_nil").appendingPathExtension("csv") 344 | 345 | // Write DataFrame 346 | try originalDF.write(toFile: fileURL.path, columnSeparator: ",") 347 | 348 | // Read DataFrame back 349 | let readDF = try DataFrame(contentsOfFile: fileURL.path, columnSeparator: ",") 350 | 351 | // Verify they are equal 352 | XCTAssertTrue(originalDF.equalsTo(dataframe: readDF)) 353 | 354 | // Clean up 355 | try fileManager.removeItem(at: fileURL) 356 | } 357 | } -------------------------------------------------------------------------------- /Tests/KoalasTests/DataFrameAdvancedTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataFrameAdvancedTests.swift 3 | // 4 | // 5 | // Created by AI Assistant on 2024. 6 | // 7 | 8 | import XCTest 9 | @testable import Koalas 10 | 11 | final class DataFrameAdvancedTests: XCTestCase { 12 | 13 | // MARK: - upscaleTransform Tests 14 | 15 | func test_upscaleTransform_TransformsDataSeriesToDataFrame() { 16 | let s1 = DataSeries([1, 2, 3, 4]) 17 | let s2 = DataSeries([5, 6, 7, 8]) 18 | 19 | let df = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 20 | 21 | let result = df.upscaleTransform { series in 22 | DataFrame(dictionaryLiteral: ("transformed", series)) 23 | } 24 | 25 | XCTAssertEqual(result["transformed"]?["col1"]?.count, 4) 26 | XCTAssertEqual(result["transformed"]?["col2"]?.count, 4) 27 | XCTAssertEqual(result["transformed"]?["col1"]?[0], 1) 28 | XCTAssertEqual(result["transformed"]?["col2"]?[0], 5) 29 | } 30 | 31 | // MARK: - flatMapValues Tests 32 | 33 | func test_flatMapValues_TransformsValues() { 34 | let s1 = DataSeries([1, 2, 3, 4]) 35 | let s2 = DataSeries([5, 6, 7, 8]) 36 | 37 | let df = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 38 | 39 | let result = df.flatMapValues { value in 40 | value.map { $0 * 2 } 41 | } 42 | 43 | XCTAssertEqual(result["col1"]?[0], 2) 44 | XCTAssertEqual(result["col1"]?[1], 4) 45 | XCTAssertEqual(result["col2"]?[0], 10) 46 | XCTAssertEqual(result["col2"]?[1], 12) 47 | } 48 | 49 | func test_flatMapValues_HandlesNilValues() { 50 | let s1 = DataSeries([1, nil, 3, 4]) 51 | let s2 = DataSeries([5, 6, nil, 8]) 52 | 53 | let df = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 54 | 55 | let result = df.flatMapValues { value in 56 | value.map { $0 * 2 } 57 | } 58 | 59 | XCTAssertEqual(result["col1"]?[0], 2) 60 | XCTAssertNil(result["col1"]?[1]) 61 | XCTAssertEqual(result["col1"]?[2], 6) 62 | XCTAssertEqual(result["col2"]?[0], 10) 63 | XCTAssertEqual(result["col2"]?[1], 12) 64 | XCTAssertNil(result["col2"]?[2]) 65 | } 66 | 67 | // MARK: - mapTo Series Tests 68 | 69 | func test_mapToSeries_WithValidSeries() { 70 | let s1 = DataSeries([1, 2, 3, 4]) 71 | let s2 = DataSeries([5, 6, 7, 8]) 72 | let targetSeries = DataSeries([10, 20, 30, 40]) 73 | 74 | let df = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 75 | 76 | let result = df.mapTo(series: targetSeries) 77 | 78 | XCTAssertNotNil(result) 79 | XCTAssertEqual(result?["col1"]?.count, 4) 80 | XCTAssertEqual(result?["col2"]?.count, 4) 81 | XCTAssertEqual(result?["col1"]?[0], 10) 82 | XCTAssertEqual(result?["col1"]?[1], 20) 83 | XCTAssertEqual(result?["col2"]?[0], 10) 84 | XCTAssertEqual(result?["col2"]?[1], 20) 85 | } 86 | 87 | func test_mapToSeries_WithNilSeries() { 88 | let s1 = DataSeries([1, 2, 3, 4]) 89 | let s2 = DataSeries([5, 6, 7, 8]) 90 | 91 | let df = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 92 | 93 | let result: DataFrame? = df.mapTo(series: nil) 94 | 95 | XCTAssertNil(result) 96 | } 97 | 98 | // func test_mapToSeries_WithDifferentLengthSeries() { 99 | // let s1 = DataSeries([1, 2, 3, 4]) 100 | // let s2 = DataSeries([5, 6, 7, 8]) 101 | // let targetSeries = DataSeries([10, 20, 30]) // Different length 102 | // 103 | // let df = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 104 | // 105 | // let result = df.mapTo(series: targetSeries) 106 | // 107 | // // Should assert and fail in debug mode, but we can't test assertions easily 108 | // // This test documents the expected behavior 109 | // XCTAssertNotNil(result) 110 | // } 111 | 112 | // MARK: - scan Tests 113 | 114 | func test_scan_WithAddition() { 115 | let s1 = DataSeries([1, 2, 3, 4]) 116 | let s2 = DataSeries([5, 6, 7, 8]) 117 | 118 | let df = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 119 | 120 | let result = df.scan(initial: 0) { current, next in 121 | (current ?? 0) + (next ?? 0) 122 | } 123 | 124 | XCTAssertEqual(result["col1"]?[0], 1) 125 | XCTAssertEqual(result["col1"]?[1], 3) 126 | XCTAssertEqual(result["col1"]?[2], 6) 127 | XCTAssertEqual(result["col1"]?[3], 10) 128 | XCTAssertEqual(result["col2"]?[0], 5) 129 | XCTAssertEqual(result["col2"]?[1], 11) 130 | XCTAssertEqual(result["col2"]?[2], 18) 131 | XCTAssertEqual(result["col2"]?[3], 26) 132 | } 133 | 134 | func test_scan_WithNilValues() { 135 | let s1 = DataSeries([1, nil, 3, 4]) 136 | let s2 = DataSeries([5, 6, nil, 8]) 137 | 138 | let df = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 139 | 140 | let result = df.scan(initial: 0) { current, next in 141 | (current ?? 0) + (next ?? 0) 142 | } 143 | 144 | XCTAssertEqual(result["col1"]?[0], 1) 145 | XCTAssertEqual(result["col1"]?[1], 1) // nil + 0 146 | XCTAssertEqual(result["col1"]?[2], 4) 147 | XCTAssertEqual(result["col1"]?[3], 8) 148 | } 149 | 150 | // MARK: - rollingFunc Tests 151 | 152 | func test_rollingFunc_WithSum() { 153 | let s1 = DataSeries([1, 2, 3, 4, 5]) 154 | let s2 = DataSeries([5, 6, 7, 8, 9]) 155 | 156 | let df = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 157 | 158 | let result = df.rollingFunc(initial: 0, window: 3) { window in 159 | window.compactMap { $0 }.reduce(0, +) 160 | } 161 | 162 | XCTAssertEqual(result["col1"]?[0], 1) // Window not full yet, but returns actual value 163 | XCTAssertEqual(result["col1"]?[1], 3) // Window not full yet, but returns actual sum 164 | XCTAssertEqual(result["col1"]?[2], 6) // 1+2+3 165 | XCTAssertEqual(result["col1"]?[3], 9) // 2+3+4 166 | XCTAssertEqual(result["col1"]?[4], 12) // 3+4+5 167 | } 168 | 169 | func test_rollingFunc_WithNilValues() { 170 | let s1 = DataSeries([1, nil, 3, 4, 5]) 171 | let s2 = DataSeries([5, 6, nil, 8, 9]) 172 | 173 | let df = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 174 | 175 | let result = df.rollingFunc(initial: 0, window: 3) { window in 176 | let validValues = window.compactMap { $0 } 177 | return validValues.isEmpty ? nil : validValues.reduce(0, +) 178 | } 179 | 180 | XCTAssertEqual(result["col1"]?[0], 1) // Window not full, but returns actual value 181 | XCTAssertEqual(result["col1"]?[1], 1) // Window not full, but returns actual sum 182 | XCTAssertEqual(result["col1"]?[2], 4) // 1+3 (nil ignored) 183 | XCTAssertEqual(result["col1"]?[3], 7) // 3+4 (nil ignored) - corrected from 8 184 | XCTAssertEqual(result["col1"]?[4], 12) // 4+5 (nil ignored) 185 | XCTAssertEqual(result["col2"]?[0], 5) 186 | XCTAssertEqual(result["col2"]?[1], 11) 187 | XCTAssertEqual(result["col2"]?[2], 11) 188 | XCTAssertEqual(result["col2"]?[3], 14) 189 | XCTAssertEqual(result["col2"]?[4], 17) 190 | } 191 | 192 | // MARK: - rollingSum Tests 193 | 194 | func test_rollingSum_WithValidData() { 195 | let s1 = DataSeries([1, 2, 3, 4, 5]) 196 | let s2 = DataSeries([5, 6, 7, 8, 9]) 197 | 198 | let df = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 199 | 200 | let result = df.rollingSum(window: 3) 201 | 202 | XCTAssertNil(result["col1"]?[0]) // Window not full, returns nil 203 | XCTAssertNil(result["col1"]?[1]) // Window not full, returns nil 204 | XCTAssertEqual(result["col1"]?[2], 6) // 1+2+3 205 | XCTAssertEqual(result["col1"]?[3], 9) // 2+3+4 206 | XCTAssertEqual(result["col1"]?[4], 12) // 3+4+5 207 | } 208 | 209 | func test_rollingSum_WithNilValues() { 210 | let s1 = DataSeries([1, nil, 3, 4, 5]) 211 | let s2 = DataSeries([5, 6, nil, 8, 9]) 212 | 213 | let df = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 214 | 215 | let result = df.rollingSum(window: 3) 216 | 217 | XCTAssertNil(result["col1"]?[0]) // Window not full, returns nil 218 | XCTAssertNil(result["col1"]?[1]) // Window not full, returns nil 219 | XCTAssertNil(result["col1"]?[2]) // Window has nil values 220 | XCTAssertNil(result["col1"]?[3]) // Window has nil values 221 | XCTAssertEqual(result["col1"]?[4], 12) // Last window has all non-nil values 222 | XCTAssertNil(result["col2"]?[0]) // Window not full, returns nil 223 | XCTAssertNil(result["col2"]?[1]) // Window not full, returns nil 224 | XCTAssertNil(result["col2"]?[2]) // Window has nil values 225 | XCTAssertNil(result["col2"]?[3]) // Window has nil values 226 | XCTAssertNil(result["col2"]?[4]) // Window has nil values - corrected from 17 227 | } 228 | 229 | // MARK: - rollingMean Tests 230 | 231 | func test_rollingMean_WithValidData() { 232 | let s1 = DataSeries([1.0, 2.0, 3.0, 4.0, 5.0]) 233 | let s2 = DataSeries([5.0, 6.0, 7.0, 8.0, 9.0]) 234 | 235 | let df = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 236 | 237 | let result = df.rollingMean(window: 3) 238 | 239 | XCTAssertNil(result["col1"]?[0]) // Window not full, returns nil 240 | XCTAssertNil(result["col1"]?[1]) // Window not full, returns nil 241 | XCTAssertEqual(result["col1"]?[2], 2.0) // (1+2+3)/3 242 | XCTAssertEqual(result["col1"]?[3], 3.0) // (2+3+4)/3 243 | XCTAssertEqual(result["col1"]?[4], 4.0) // (3+4+5)/3 244 | } 245 | 246 | func test_rollingMean_WithNilValues() { 247 | let s1 = DataSeries([1.0, nil, 3.0, 4.0, 5.0]) 248 | let s2 = DataSeries([5.0, 6.0, nil, 8.0, 9.0]) 249 | 250 | let df = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 251 | 252 | let result = df.rollingMean(window: 3) 253 | 254 | XCTAssertNil(result["col1"]?[0]) // Window not full, returns nil 255 | XCTAssertNil(result["col1"]?[1]) // Window not full, returns nil 256 | XCTAssertNil(result["col1"]?[2]) // Window has nil values 257 | XCTAssertNil(result["col1"]?[3]) // Window has nil values 258 | XCTAssertEqual(result["col1"]?[4], 4.0) // Last window has all non-nil values 259 | } 260 | 261 | // MARK: - shape Tests 262 | 263 | func test_shape_ReturnsCorrectDimensions() { 264 | let s1 = DataSeries([1, 2, 3, 4]) 265 | let s2 = DataSeries([5, 6, 7, 8]) 266 | let s3 = DataSeries([9, 10, 11, 12]) 267 | 268 | let df = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2), ("col3", s3)) 269 | 270 | let shape = df.shape() 271 | 272 | XCTAssertEqual(shape.width, 3) // 3 columns 273 | XCTAssertEqual(shape.height, 4) // 4 rows 274 | } 275 | 276 | func test_shape_WithEmptyDataFrame() { 277 | let df: DataFrame = [:] 278 | 279 | let shape = df.shape() 280 | 281 | XCTAssertEqual(shape.width, 0) 282 | XCTAssertEqual(shape.height, 0) 283 | } 284 | 285 | // MARK: - mean Tests 286 | 287 | func test_mean_WithValidData() { 288 | let s1 = DataSeries([1.0, 2.0, 3.0, 4.0]) 289 | let s2 = DataSeries([5.0, 6.0, 7.0, 8.0]) 290 | 291 | let df = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 292 | 293 | let result = df.mean(shouldSkipNils: true) 294 | 295 | XCTAssertEqual(result["col1"]?.count, 1) 296 | XCTAssertEqual(result["col2"]?.count, 1) 297 | XCTAssertEqual(result["col1"]?[0], 2.5) // (1+2+3+4)/4 298 | XCTAssertEqual(result["col2"]?[0], 6.5) // (5+6+7+8)/4 299 | } 300 | 301 | func test_mean_WithNilValues() { 302 | let s1 = DataSeries([1.0, nil, 3.0, 4.0]) 303 | let s2 = DataSeries([5.0, 6.0, nil, 8.0]) 304 | 305 | let df = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 306 | 307 | let result = df.mean(shouldSkipNils: true) 308 | 309 | XCTAssertEqual(result["col1"]?[0], 2.6666666666666665) // (1+3+4)/3 310 | XCTAssertEqual(result["col2"]?[0], 6.333333333333333) // (5+6+8)/3 311 | } 312 | 313 | func test_mean_WithNilValuesNotSkipped() { 314 | let s1 = DataSeries([1.0, nil, 3.0, 4.0]) 315 | let s2 = DataSeries([5.0, 6.0, nil, 8.0]) 316 | 317 | let df = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 318 | 319 | let result = df.mean(shouldSkipNils: false) 320 | 321 | XCTAssertEqual(result["col1"]?[0], 2.0) // (1+0+3+4)/4 322 | XCTAssertEqual(result["col2"]?[0], 4.75) // (5+6+0+8)/4 323 | } 324 | 325 | // MARK: - std Tests 326 | 327 | func test_std_WithValidData() { 328 | let s1 = DataSeries([1.0, 2.0, 3.0, 4.0]) 329 | let s2 = DataSeries([5.0, 6.0, 7.0, 8.0]) 330 | 331 | let df = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 332 | 333 | let result = df.std(shouldSkipNils: true) 334 | 335 | XCTAssertEqual(result["col1"]?.count, 1) 336 | XCTAssertEqual(result["col2"]?.count, 1) 337 | // Expected values calculated manually 338 | XCTAssertEqual(result["col1"]?[0] ?? 0.0, 1.2909944487358056, accuracy: 0.0001) 339 | XCTAssertEqual(result["col2"]?[0] ?? 0.0, 1.2909944487358056, accuracy: 0.0001) 340 | } 341 | 342 | func test_std_WithNilValues() { 343 | let s1 = DataSeries([1.0, nil, 3.0, 4.0]) 344 | let s2 = DataSeries([5.0, 6.0, nil, 8.0]) 345 | 346 | let df = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 347 | 348 | let result = df.std(shouldSkipNils: true) 349 | 350 | // Expected values calculated manually for (1,3,4) and (5,6,8) 351 | XCTAssertEqual(result["col1"]?[0] ?? 0.0, 1.5275252316519468, accuracy: 0.0001) 352 | XCTAssertEqual(result["col2"]?[0] ?? 0.0, 1.5275252316519468, accuracy: 0.0001) 353 | } 354 | 355 | func test_std_WithSingleValue() { 356 | let s1 = DataSeries([1.0]) 357 | let s2 = DataSeries([5.0]) 358 | 359 | let df = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 360 | 361 | let result = df.std(shouldSkipNils: true) 362 | 363 | XCTAssertNil(result["col1"]?[0]) // Need at least 2 values for std 364 | XCTAssertNil(result["col2"]?[0]) // Need at least 2 values for std 365 | } 366 | } -------------------------------------------------------------------------------- /Sources/Koalas/DataSeries/SeriesArrayExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Sergey Kazakov on 25.06.2020. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension SeriesArray { 11 | /** 12 | Applies a conditional operation based on boolean values in the series. 13 | Returns trueSeries values where the condition is true, else series values where false. 14 | Returns nil if either input series is nil. 15 | */ 16 | func whereTrue(then trueSeries: DataSeries?, else series: DataSeries?) -> DataSeries? where Element == Bool? { 17 | guard let trueSeries = trueSeries, 18 | let series = series 19 | else { 20 | return nil 21 | } 22 | 23 | let zip3 = zipSeriesArray(s1: self, s2: trueSeries, s3: series) 24 | 25 | let resultArray = zip3.map { zipped in zipped.0.map { $0 ? zipped.1 : zipped.2 } ?? nil } 26 | return DataSeries(resultArray) 27 | } 28 | } 29 | 30 | public extension SeriesArray { 31 | /** 32 | Checks if the series contains only nil values. 33 | Returns true if all elements are nil or if the series is empty. 34 | */ 35 | func isEmptySeries() -> Bool where Element == T?, T: Equatable { 36 | guard let firstNonNil = first(where: { $0 != nil }) else { 37 | return true 38 | } 39 | 40 | return firstNonNil == nil 41 | } 42 | 43 | /** 44 | Compares this series with another series for equality. 45 | Returns true if both series have the same length and corresponding elements are equal. 46 | */ 47 | func equalsTo(series: DataSeries?) -> Bool where Element == T?, T: Equatable { 48 | guard let series = series else { 49 | return false 50 | } 51 | 52 | guard count == series.count else { 53 | return false 54 | } 55 | 56 | return zip(self, series).first { !isElementEqual(lhs: $0.0, rhs: $0.1) } == nil 57 | } 58 | 59 | /** 60 | Compares this series with another series for equality with precision tolerance. 61 | Useful for floating-point comparisons where exact equality is not required. 62 | */ 63 | func equalsTo(series: DataSeries?, with precision: T) -> Bool where Element == T?, T: FloatingPoint { 64 | guard let series = series else { 65 | return false 66 | } 67 | 68 | guard count == series.count else { 69 | return false 70 | } 71 | 72 | return zip(self, series).first { !isElementEqual(lhs: $0.0, rhs: $0.1, with: precision) } == nil 73 | } 74 | 75 | /** 76 | Compares this series with another series for equality. 77 | Returns true if both series have the same length and corresponding elements are equal. 78 | */ 79 | func equalsTo(series: DataSeries?) -> Bool where Element == T?, T: Numeric { 80 | guard let series = series else { 81 | return false 82 | } 83 | 84 | guard count == series.count else { 85 | return false 86 | } 87 | 88 | return zip(self, series).first { !isElementEqual(lhs: $0.0, rhs: $0.1) } == nil 89 | } 90 | 91 | /** 92 | Fills all nil values in the series with a specified value. 93 | Returns a new series with nil values replaced by the provided value. 94 | */ 95 | func fillNils(with value: Element) -> DataSeries where Element == T? { 96 | return DataSeries(map { $0 ?? value } ) 97 | } 98 | 99 | /** 100 | Fills nil values using the specified method (all, backward, or forward fill). 101 | Returns a new series with nil values filled according to the method. 102 | */ 103 | func fillNils(method: FillNilsMethod) -> DataSeries where Element == T? { 104 | switch method { 105 | case .all(let value): 106 | return fillNils(with: value) 107 | case .backward(let initial): 108 | let res = DataSeries(reversed()).scan(initial: initial) { ($1 ?? $0) } 109 | return DataSeries(res.reversed()) 110 | case .forward(let initial): 111 | let res = scan(initial: initial) { ($1 ?? $0) } 112 | return DataSeries(res) 113 | } 114 | } 115 | 116 | /** 117 | Creates a new series with all elements set to a constant value. 118 | Returns a series of the same length with every element equal to the specified value. 119 | */ 120 | func mapTo(constant value: T) -> DataSeries { 121 | return DataSeries(repeating: value, count: self.count) 122 | } 123 | 124 | /** 125 | Shifts the series by the specified number of positions. 126 | Positive values shift forward (add nils at beginning), negative values shift backward (add nils at end). 127 | */ 128 | func shiftedBy(_ k: Int) -> DataSeries where Element == T? { 129 | let shift = abs(k) 130 | guard k > 0 else { 131 | var arr = self 132 | arr.append(contentsOf: DataSeries(repeating: nil, count: shift)) 133 | arr.removeFirst(shift) 134 | return arr 135 | } 136 | 137 | var arr = self 138 | arr.insert(contentsOf: DataSeries(repeating: nil, count: shift), at: 0) 139 | arr.removeLast(shift) 140 | return arr 141 | } 142 | 143 | /** 144 | Calculates the sum of all non-nil values in the series. 145 | Returns nil if ignoreNils is false and there are nil values present. 146 | */ 147 | func sum(ignoreNils: Bool = true) -> T? where Element == T?, T: Numeric { 148 | let nonNils = filter { $0 != nil } 149 | guard ignoreNils || nonNils.count == count else { 150 | return nil 151 | } 152 | 153 | return nonNils.map { $0 ?? 0 }.reduce(0, +) 154 | } 155 | 156 | /** 157 | Calculates the mean of all values in the series. 158 | If shouldSkipNils is true, only non-nil values are considered. Otherwise, nils are treated as 0. 159 | */ 160 | func mean(shouldSkipNils: Bool = true) -> T? where Element == T?, T: FloatingPoint { 161 | let nonNils = shouldSkipNils ? 162 | DataSeries(self.filter { $0 != nil }) : 163 | self.fillNils(with: 0) 164 | 165 | guard nonNils.count > 0 else { 166 | return nil 167 | } 168 | 169 | let sum = nonNils.map { $0 ?? 0 }.reduce(0, +) 170 | 171 | return sum / T(nonNils.count) 172 | } 173 | 174 | /** 175 | Calculates the standard deviation of all values in the series. 176 | If shouldSkipNils is true, only non-nil values are considered. Otherwise, nils are treated as 0. 177 | */ 178 | func std(shouldSkipNils: Bool = true) -> T? where Element == T?, T: FloatingPoint { 179 | let nonNils = shouldSkipNils ? 180 | DataSeries(self.filter { $0 != nil }) : 181 | self.fillNils(with: 0) 182 | 183 | guard nonNils.count > 1 else { 184 | return nil 185 | } 186 | 187 | let sum = nonNils.map { $0 ?? 0 }.reduce(0, +) 188 | let mean = sum / T(nonNils.count) 189 | 190 | 191 | let diff = nonNils - nonNils.mapTo(constant: mean) 192 | let squaredDiffSum = (diff * diff)?.map { $0 ?? 0 }.reduce(0, +) 193 | let squaredStd = (squaredDiffSum ?? 0) / T(nonNils.count - 1) 194 | return sqrt(squaredStd) 195 | } 196 | 197 | /** 198 | Calculates expanding (cumulative) sum of the series. 199 | Returns a series where each element is the sum of all previous elements plus the current element. 200 | */ 201 | func expandingSum(initial: T) -> DataSeries where Element == T?, T: Numeric { 202 | let res = scan(initial: initial) { $0 + ($1 ?? 0) } 203 | return DataSeries(res) 204 | } 205 | 206 | /** 207 | Calculates expanding (cumulative) maximum of the series. 208 | Returns a series where each element is the maximum of all previous elements and the current element. 209 | */ 210 | func expandingMax() -> DataSeries where Element == T?, T: Comparable { 211 | let res = scan(initial: first ?? nil) { current, next in 212 | guard let next = next else { 213 | return current 214 | } 215 | 216 | guard let current = current else { 217 | return next 218 | } 219 | 220 | return Swift.max(current, next) 221 | 222 | } 223 | 224 | return DataSeries(res) 225 | } 226 | 227 | /** 228 | Calculates expanding (cumulative) minimum of the series. 229 | Returns a series where each element is the minimum of all previous elements and the current element. 230 | */ 231 | func expandingMin() -> DataSeries where Element == T?, T: Comparable { 232 | let res = scan(initial: first ?? nil) { current, next in 233 | guard let next = next else { 234 | return current 235 | } 236 | 237 | guard let current = current else { 238 | return next 239 | } 240 | 241 | return Swift.min(current, next) 242 | 243 | } 244 | 245 | return DataSeries(res) 246 | } 247 | 248 | /** 249 | Applies a rolling window function to the series. 250 | Uses the specified window size and custom function to process each window of elements. 251 | */ 252 | func rollingFunc(initial: T?, window: Int, windowFunc: (([Element]) -> Element)) -> DataSeries where Element == T?, T: Numeric { 253 | let res = rollingScan(initial: initial, window: window, windowFunc: windowFunc) 254 | return DataSeries(res) 255 | } 256 | 257 | /** 258 | Calculates rolling sum with the specified window size. 259 | Returns a series where each element is the sum of the current element and the previous (window-1) elements. 260 | */ 261 | func rollingSum(window: Int) -> DataSeries where Element == T?, T: Numeric { 262 | let res = rollingScan(initial: nil, window: window) { windowArray in 263 | guard windowArray.allSatisfy({ $0 != nil }) else { 264 | return nil 265 | } 266 | 267 | return windowArray.reduce(0) { $0 + ($1 ?? 0) } 268 | } 269 | 270 | return DataSeries(res) 271 | } 272 | 273 | /** 274 | Calculates rolling mean with the specified window size. 275 | Returns a series where each element is the mean of the current element and the previous (window-1) elements. 276 | */ 277 | func rollingMean(window: Int) -> DataSeries where Element == T?, T: FloatingPoint { 278 | let res = rollingScan(initial: nil, window: window) { windowArray in 279 | guard windowArray.allSatisfy({ $0 != nil }) else { 280 | return nil 281 | } 282 | 283 | return windowArray.reduce(0) { $0 + ($1 ?? 0) } / T(windowArray.count) 284 | } 285 | 286 | return DataSeries(res) 287 | } 288 | 289 | /** 290 | Applies a scan operation to the series with a custom transformation function. 291 | Performs cumulative operations with an initial value and transformation function. 292 | */ 293 | func scanSeries(initial: T?, _ nextPartialResult: (_ current: T?, _ next: U?) -> T?) -> DataSeries where Element == U? { 294 | let res = scan(initial: initial, nextPartialResult) 295 | return DataSeries(res) 296 | } 297 | } 298 | 299 | public extension SeriesArray { 300 | /** 301 | Safely accesses an element at the specified index. 302 | Returns nil if the index is out of bounds. 303 | */ 304 | func at(index: Int) -> Element? { 305 | guard index >= 0 && index < count else { 306 | return nil 307 | } 308 | 309 | return self[index] 310 | } 311 | 312 | /** 313 | Safely sets an element at the specified index. 314 | Returns the original series unchanged if the index is out of bounds. 315 | */ 316 | func setAt(index: Int, value: Element) -> Self { 317 | guard index >= 0 && index < count else { 318 | return self 319 | } 320 | 321 | var array = self 322 | 323 | array[index] = value 324 | return array 325 | } 326 | } 327 | 328 | fileprivate extension SeriesArray { 329 | /** 330 | Performs a scan operation with a custom transformation function. 331 | Returns an array where each element is the result of applying the function to all previous elements. 332 | */ 333 | func scan(initial: T, _ f: (T, Element) -> T) -> [T] { 334 | var result = self.reduce([initial]) { (listSoFar: [T], next: Element) -> [T] in 335 | let lastElement = listSoFar.last ?? initial 336 | return listSoFar + [f(lastElement, next)] 337 | } 338 | 339 | result.removeFirst() 340 | return result 341 | } 342 | 343 | /** 344 | Performs a rolling scan operation with a custom window function. 345 | Applies the window function to each window of elements as the series is processed. 346 | */ 347 | func rollingScan(initial: Element, window: Int, windowFunc: (([Element]) -> Element)) -> Array { 348 | let initialWindowArray = Array(repeating: initial, count: window) 349 | let f: ([Element], Element) -> [Element] = { 350 | var arr = $0 351 | arr.append($1) 352 | let _ = arr.removeFirst() 353 | 354 | return arr 355 | } 356 | 357 | var result = reduce([initialWindowArray]) { (listSoFar: [[Element]], next: Element) -> [[Element]] in 358 | let lastElement = listSoFar.last ?? initialWindowArray 359 | return listSoFar + [f(lastElement, next)] 360 | } 361 | 362 | result.removeFirst() 363 | return result.map { windowFunc($0) } 364 | } 365 | } 366 | 367 | /** 368 | Compares two optional floating-point values for equality with precision tolerance. 369 | Returns true if the absolute difference is less than or equal to the precision value. 370 | */ 371 | fileprivate func isElementEqual(lhs: T?, rhs: T?, with precision: T) -> Bool where T: FloatingPoint { 372 | if lhs == nil && rhs == nil { 373 | return true 374 | } 375 | 376 | guard let lhs = lhs, let rhs = rhs else { 377 | return false 378 | } 379 | 380 | guard !lhs.isEqual(to: rhs) else { 381 | return true 382 | } 383 | 384 | return abs(lhs - rhs) <= precision 385 | } 386 | 387 | /** 388 | Compares two optional values for equality. 389 | Returns true if both values are nil or if both values are equal. 390 | */ 391 | fileprivate func isElementEqual(lhs: T?, rhs: T?) -> Bool where T: Equatable { 392 | if lhs == nil && rhs == nil { 393 | return true 394 | } 395 | 396 | guard let lhs = lhs, let rhs = rhs else { 397 | return false 398 | } 399 | 400 | return lhs == rhs 401 | } 402 | 403 | /** 404 | Combines three SeriesArrays into a single array of tuples. 405 | Asserts that all arrays have equal length and returns corresponding elements as tuples. 406 | */ 407 | func zipSeriesArray(s1: SeriesArray, s2: SeriesArray, s3: SeriesArray) -> Array<(T1, T2, T3)> { 408 | assert(s1.count == s2.count, "Dataseries should have equal length") 409 | assert(s1.count == s3.count, "Dataseries should have equal length") 410 | 411 | return zip(s1, zip(s2, s3)).map { ($0.0, $0.1.0, $0.1.1) } 412 | } 413 | -------------------------------------------------------------------------------- /Sources/Koalas/DataSeries/DataSeries+Arithmetics.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Sergey Kazakov on 25.06.2020. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Performs element-wise addition between two optional DataSeries. 12 | Returns nil if either series is nil. 13 | */ 14 | public func + (lhs: DataSeries?, rhs: DataSeries?) -> DataSeries? where T: Numeric { 15 | unwrap(lhs, rhs) { $0 + $1 } 16 | } 17 | 18 | /** 19 | Performs element-wise subtraction between two optional DataSeries. 20 | Returns nil if either series is nil. 21 | */ 22 | public func - (lhs: DataSeries?, rhs: DataSeries?) -> DataSeries? where T: Numeric { 23 | unwrap(lhs, rhs) { $0 - $1 } 24 | } 25 | 26 | /** 27 | Performs element-wise multiplication between two optional DataSeries. 28 | Returns nil if either series is nil. 29 | */ 30 | public func * (lhs: DataSeries?, rhs: DataSeries?) -> DataSeries? where T: Numeric { 31 | unwrap(lhs, rhs) { $0 * $1 } 32 | } 33 | 34 | /** 35 | Performs element-wise division between two optional DataSeries. 36 | Returns nil if either series is nil. 37 | */ 38 | public func / (lhs: DataSeries?, rhs: DataSeries?) -> DataSeries? where T: FloatingPoint { 39 | unwrap(lhs, rhs) { $0 / $1 } 40 | } 41 | 42 | /** 43 | Performs element-wise inequality comparison between two optional DataSeries. 44 | Returns a boolean DataSeries indicating where elements are not equal. 45 | */ 46 | public func != (lhs: DataSeries?, rhs: DataSeries?) -> DataSeries? where T: Equatable { 47 | unwrap(lhs, rhs) { $0 != $1 } 48 | } 49 | 50 | /** 51 | Performs element-wise equality comparison between two optional DataSeries. 52 | Returns a boolean DataSeries indicating where elements are equal. 53 | */ 54 | public func == (lhs: DataSeries?, rhs: DataSeries?) -> DataSeries? where T: Equatable { 55 | unwrap(lhs, rhs) { $0 == $1 } 56 | } 57 | 58 | /** 59 | Performs element-wise equality comparison between an optional DataSeries and a scalar. 60 | Returns a boolean DataSeries indicating where elements equal the scalar. 61 | */ 62 | public func == (lhs: DataSeries?, rhs: T?) -> DataSeries? where T: Comparable { 63 | unwrap(lhs, rhs) { $0 == $1 } 64 | } 65 | 66 | /** 67 | Performs element-wise inequality comparison between an optional DataSeries and a scalar. 68 | Returns a boolean DataSeries indicating where elements are not equal to the scalar. 69 | */ 70 | public func != (lhs: DataSeries?, rhs: T?) -> DataSeries? where T: Comparable { 71 | unwrap(lhs, rhs) { $0 != $1 } 72 | } 73 | 74 | /** 75 | Performs element-wise less than comparison between two optional DataSeries. 76 | Returns a boolean DataSeries indicating where left elements are < right elements. 77 | */ 78 | public func < (lhs: DataSeries?, rhs: DataSeries?) -> DataSeries? where T: Comparable { 79 | unwrap(lhs, rhs) { $0 < $1 } 80 | } 81 | 82 | /** 83 | Performs element-wise less than or equal comparison between two optional DataSeries. 84 | Returns a boolean DataSeries indicating where left elements are <= right elements. 85 | */ 86 | public func <= (lhs: DataSeries?, rhs: DataSeries?) -> DataSeries? where T: Comparable { 87 | unwrap(lhs, rhs) { $0 <= $1 } 88 | } 89 | 90 | /** 91 | Performs element-wise greater than comparison between two optional DataSeries. 92 | Returns a boolean DataSeries indicating where left elements are > right elements. 93 | */ 94 | public func > (lhs: DataSeries?, rhs: DataSeries?) -> DataSeries? where T: Comparable { 95 | unwrap(lhs, rhs) { $0 > $1 } 96 | } 97 | 98 | /** 99 | Performs element-wise greater than or equal comparison between two optional DataSeries. 100 | Returns a boolean DataSeries indicating where left elements are >= right elements. 101 | */ 102 | public func >= (lhs: DataSeries?, rhs: DataSeries?) -> DataSeries? where T: Comparable { 103 | unwrap(lhs, rhs) { $0 >= $1 } 104 | } 105 | 106 | /** 107 | Performs element-wise less than comparison between an optional DataSeries and a scalar. 108 | Returns a boolean DataSeries indicating where elements are < the scalar. 109 | */ 110 | public func < (lhs: DataSeries?, rhs: T?) -> DataSeries? where T: Comparable { 111 | unwrap(lhs, rhs) { $0 < $1 } 112 | } 113 | 114 | /** 115 | Performs element-wise less than or equal comparison between an optional DataSeries and a scalar. 116 | Returns a boolean DataSeries indicating where elements are <= the scalar. 117 | */ 118 | public func <= (lhs: DataSeries?, rhs: T?) -> DataSeries? where T: Comparable { 119 | unwrap(lhs, rhs) { $0 <= $1 } 120 | } 121 | 122 | /** 123 | Performs element-wise greater than comparison between an optional DataSeries and a scalar. 124 | Returns a boolean DataSeries indicating where elements are > the scalar. 125 | */ 126 | public func > (lhs: DataSeries?, rhs: T?) -> DataSeries? where T: Comparable { 127 | unwrap(lhs, rhs) { $0 > $1 } 128 | } 129 | 130 | /** 131 | Performs element-wise greater than or equal comparison between an optional DataSeries and a scalar. 132 | Returns a boolean DataSeries indicating where elements are >= the scalar. 133 | */ 134 | public func >= (lhs: DataSeries?, rhs: T?) -> DataSeries? where T: Comparable { 135 | unwrap(lhs, rhs) { $0 >= $1 } 136 | } 137 | 138 | /** 139 | Performs element-wise logical AND between two optional boolean DataSeries. 140 | Returns a boolean DataSeries with the logical AND of corresponding elements. 141 | */ 142 | public func && (lhs: DataSeries?, rhs: DataSeries?) -> DataSeries? { 143 | unwrap(lhs, rhs) { $0 && $1 } 144 | } 145 | 146 | /** 147 | Performs element-wise logical OR between two optional boolean DataSeries. 148 | Returns a boolean DataSeries with the logical OR of corresponding elements. 149 | */ 150 | public func || (lhs: DataSeries?, rhs: DataSeries?) -> DataSeries? { 151 | unwrap(lhs, rhs) { $0 || $1 } 152 | } 153 | 154 | /** 155 | Performs element-wise addition between two DataSeries. 156 | Asserts that both series have equal length and returns the sum of corresponding elements. 157 | */ 158 | public func + (lhs: DataSeries, rhs: DataSeries) -> DataSeries where T: Numeric { 159 | assert(lhs.count == rhs.count, "Dataseries should have equal length") 160 | 161 | let res = zip(lhs, rhs).map { 162 | unwrap($0.0, $0.1) { $0 + $1 } 163 | } 164 | 165 | return DataSeries(res) 166 | } 167 | 168 | /** 169 | Performs element-wise subtraction between two DataSeries. 170 | Asserts that both series have equal length and returns the difference of corresponding elements. 171 | */ 172 | public func - (lhs: DataSeries, rhs: DataSeries) -> DataSeries where T: Numeric { 173 | assert(lhs.count == rhs.count, "Dataseries should have equal length") 174 | 175 | let res = zip(lhs, rhs).map { 176 | unwrap($0.0, $0.1) { $0 - $1 } 177 | } 178 | 179 | return DataSeries(res) 180 | } 181 | 182 | /** 183 | Performs element-wise multiplication between two DataSeries. 184 | Asserts that both series have equal length and returns the product of corresponding elements. 185 | */ 186 | public func * (lhs: DataSeries, rhs: DataSeries) -> DataSeries where T: Numeric { 187 | assert(lhs.count == rhs.count, "Dataseries should have equal length") 188 | 189 | let res = zip(lhs, rhs).map { 190 | unwrap($0.0, $0.1) { $0 * $1 } 191 | } 192 | 193 | return DataSeries(res) 194 | } 195 | 196 | /** 197 | Performs element-wise division between two DataSeries. 198 | Asserts that both series have equal length and returns the quotient of corresponding elements. 199 | */ 200 | public func / (lhs: DataSeries, rhs: DataSeries) -> DataSeries where T: FloatingPoint { 201 | assert(lhs.count == rhs.count, "Dataseries should have equal length") 202 | 203 | let res = zip(lhs, rhs).map { 204 | unwrap($0.0, $0.1) { $0 / $1 } 205 | } 206 | 207 | return DataSeries(res) 208 | } 209 | 210 | /** 211 | Performs element-wise less than comparison between two DataSeries. 212 | Asserts that both series have equal length and returns boolean values for < comparison. 213 | */ 214 | public func < (lhs: DataSeries, rhs: DataSeries) -> DataSeries where T: Comparable { 215 | assert(lhs.count == rhs.count, "Dataseries should have equal length") 216 | 217 | let res = zip(lhs, rhs).map { 218 | unwrap($0.0, $0.1) { $0 < $1 } 219 | } 220 | 221 | return DataSeries(res) 222 | } 223 | 224 | /** 225 | Performs element-wise less than or equal comparison between two DataSeries. 226 | Asserts that both series have equal length and returns boolean values for <= comparison. 227 | */ 228 | public func <= (lhs: DataSeries, rhs: DataSeries) -> DataSeries where T: Comparable { 229 | assert(lhs.count == rhs.count, "Dataseries should have equal length") 230 | 231 | let res = zip(lhs, rhs).map { 232 | unwrap($0.0, $0.1) { $0 <= $1 } 233 | } 234 | 235 | return DataSeries(res) 236 | } 237 | 238 | /** 239 | Performs element-wise greater than comparison between two DataSeries. 240 | Asserts that both series have equal length and returns boolean values for > comparison. 241 | */ 242 | public func > (lhs: DataSeries, rhs: DataSeries) -> DataSeries where T: Comparable { 243 | assert(lhs.count == rhs.count, "Dataseries should have equal length") 244 | 245 | let res = zip(lhs, rhs).map { 246 | unwrap($0.0, $0.1) { $0 > $1 } 247 | } 248 | 249 | return DataSeries(res) 250 | } 251 | 252 | /** 253 | Performs element-wise greater than or equal comparison between two DataSeries. 254 | Asserts that both series have equal length and returns boolean values for >= comparison. 255 | */ 256 | public func >= (lhs: DataSeries, rhs: DataSeries) -> DataSeries where T: Comparable { 257 | assert(lhs.count == rhs.count, "Dataseries should have equal length") 258 | 259 | let res = zip(lhs, rhs).map { 260 | unwrap($0.0, $0.1) { $0 >= $1 } 261 | } 262 | 263 | return DataSeries(res) 264 | } 265 | 266 | /** 267 | Performs element-wise inequality comparison between two DataSeries. 268 | Asserts that both series have equal length and returns boolean values for != comparison. 269 | */ 270 | public func != (lhs: DataSeries, rhs: DataSeries) -> DataSeries where T: Equatable { 271 | assert(lhs.count == rhs.count, "Dataseries should have equal length") 272 | 273 | let res = zip(lhs, rhs).map { 274 | unwrap($0.0, $0.1) { $0 != $1 } 275 | } 276 | 277 | return DataSeries(res) 278 | } 279 | 280 | /** 281 | Performs element-wise equality comparison between two DataSeries. 282 | Asserts that both series have equal length and returns boolean values for == comparison. 283 | */ 284 | public func == (lhs: DataSeries, rhs: DataSeries) -> DataSeries where T: Equatable { 285 | assert(lhs.count == rhs.count, "Dataseries should have equal length") 286 | 287 | let res = zip(lhs, rhs).map { 288 | unwrap($0.0, $0.1) { $0 == $1 } 289 | } 290 | 291 | return DataSeries(res) 292 | } 293 | 294 | /** 295 | Performs element-wise equality comparison between a DataSeries and a scalar. 296 | Converts the scalar to a DataSeries and performs the comparison. 297 | */ 298 | public func == (lhs: DataSeries, rhs: T) -> DataSeries where T: Comparable { 299 | return lhs == lhs.mapTo(constant: rhs) 300 | } 301 | 302 | /** 303 | Performs element-wise inequality comparison between a DataSeries and a scalar. 304 | Converts the scalar to a DataSeries and performs the comparison. 305 | */ 306 | public func != (lhs: DataSeries, rhs: T) -> DataSeries where T: Comparable { 307 | return lhs != lhs.mapTo(constant: rhs) 308 | } 309 | 310 | /** 311 | Performs element-wise less than comparison between a DataSeries and a scalar. 312 | Converts the scalar to a DataSeries and performs the comparison. 313 | */ 314 | public func < (lhs: DataSeries, rhs: T) -> DataSeries where T: Comparable { 315 | return lhs < lhs.mapTo(constant: rhs) 316 | } 317 | 318 | /** 319 | Performs element-wise less than or equal comparison between a DataSeries and a scalar. 320 | Converts the scalar to a DataSeries and performs the comparison. 321 | */ 322 | public func <= (lhs: DataSeries, rhs: T) -> DataSeries where T: Comparable { 323 | return lhs <= lhs.mapTo(constant: rhs) 324 | } 325 | 326 | /** 327 | Performs element-wise greater than comparison between a DataSeries and a scalar. 328 | Converts the scalar to a DataSeries and performs the comparison. 329 | */ 330 | public func > (lhs: DataSeries, rhs: T) -> DataSeries where T: Comparable { 331 | return lhs > lhs.mapTo(constant: rhs) 332 | } 333 | 334 | /** 335 | Performs element-wise greater than or equal comparison between a DataSeries and a scalar. 336 | Converts the scalar to a DataSeries and performs the comparison. 337 | */ 338 | public func >= (lhs: DataSeries, rhs: T) -> DataSeries where T: Comparable { 339 | return lhs >= lhs.mapTo(constant: rhs) 340 | } 341 | 342 | /** 343 | Performs element-wise addition between a DataSeries and a scalar. 344 | Converts the scalar to a DataSeries and performs the addition. 345 | */ 346 | public func + (lhs: DataSeries, rhs: T) -> DataSeries where T: Numeric { 347 | return lhs + lhs.mapTo(constant: rhs) 348 | } 349 | 350 | /** 351 | Performs element-wise subtraction between a DataSeries and a scalar. 352 | Converts the scalar to a DataSeries and performs the subtraction. 353 | */ 354 | public func - (lhs: DataSeries, rhs: T) -> DataSeries where T: Numeric { 355 | return lhs - lhs.mapTo(constant: rhs) 356 | } 357 | 358 | /** 359 | Performs element-wise multiplication between a DataSeries and a scalar. 360 | Converts the scalar to a DataSeries and performs the multiplication. 361 | */ 362 | public func * (lhs: DataSeries, rhs: T) -> DataSeries where T: Numeric { 363 | return lhs * lhs.mapTo(constant: rhs) 364 | } 365 | 366 | /** 367 | Performs element-wise division between a DataSeries and a scalar. 368 | Converts the scalar to a DataSeries and performs the division. 369 | */ 370 | public func / (lhs: DataSeries, rhs: T) -> DataSeries where T: FloatingPoint { 371 | return lhs / lhs.mapTo(constant: rhs) 372 | } 373 | 374 | /** 375 | Performs element-wise subtraction between an optional DataSeries and a scalar. 376 | Returns nil if the DataSeries is nil. 377 | */ 378 | public func - (lhs: DataSeries?, rhs: T?) -> DataSeries? where T: Numeric { 379 | unwrap(lhs, rhs) { $0 - $1 } 380 | } 381 | 382 | /** 383 | Performs element-wise addition between an optional DataSeries and a scalar. 384 | Returns nil if the DataSeries is nil. 385 | */ 386 | public func + (lhs: DataSeries?, rhs: T?) -> DataSeries? where T: Numeric { 387 | unwrap(lhs, rhs) { $0 + $1 } 388 | } 389 | 390 | /** 391 | Performs element-wise multiplication between an optional DataSeries and a scalar. 392 | Returns nil if the DataSeries is nil. 393 | */ 394 | public func * (lhs: DataSeries?, rhs: T?) -> DataSeries? where T: Numeric { 395 | unwrap(lhs, rhs) { $0 * $1 } 396 | } 397 | 398 | /** 399 | Performs element-wise division between an optional DataSeries and a scalar. 400 | Returns nil if the DataSeries is nil. 401 | */ 402 | public func / (lhs: DataSeries?, rhs: T?) -> DataSeries? where T: FloatingPoint { 403 | unwrap(lhs, rhs) { $0 / $1 } 404 | } 405 | 406 | /** 407 | Performs element-wise logical AND between two boolean DataSeries. 408 | Asserts that both series have equal length and returns boolean values for logical AND. 409 | */ 410 | public func && (lhs: DataSeries, rhs: DataSeries) -> DataSeries { 411 | assert(lhs.count == rhs.count, "Dataseries should have equal length") 412 | 413 | let res = zip(lhs, rhs).map { 414 | unwrap($0.0, $0.1) { $0 && $1 } 415 | } 416 | 417 | return DataSeries(res) 418 | } 419 | 420 | /** 421 | Performs element-wise logical OR between two boolean DataSeries. 422 | Asserts that both series have equal length and returns boolean values for logical OR. 423 | */ 424 | public func || (lhs: DataSeries, rhs: DataSeries) -> DataSeries { 425 | assert(lhs.count == rhs.count, "Dataseries should have equal length") 426 | 427 | let res = zip(lhs, rhs).map { 428 | unwrap($0.0, $0.1) { $0 || $1 } 429 | } 430 | 431 | return DataSeries(res) 432 | } 433 | -------------------------------------------------------------------------------- /Sources/Koalas/DataFrame/DataFrame+Arithmetics.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Sergey Kazakov on 25.06.2020. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Performs element-wise inequality comparison between two DataFrames. 12 | Returns a DataFrame of boolean values indicating where elements are not equal. 13 | */ 14 | public func != (lhs: DataFrame?, 15 | rhs: DataFrame?) -> DataFrame? where T: Equatable { 16 | return unwrap(lhs, rhs) { $0 != $1 } 17 | } 18 | 19 | /** 20 | Performs element-wise equality comparison between two DataFrames. 21 | Returns a DataFrame of boolean values indicating where elements are equal. 22 | */ 23 | public func == (lhs: DataFrame?, 24 | rhs: DataFrame?) -> DataFrame? where T: Equatable { 25 | unwrap(lhs, rhs) { $0 == $1 } 26 | } 27 | 28 | /** 29 | Performs element-wise inequality comparison between a DataFrame and a scalar value. 30 | Returns a DataFrame of boolean values indicating where elements are not equal to the scalar. 31 | */ 32 | public func != (lhs: DataFrame?, 33 | rhs: T?) -> DataFrame? where T: Equatable { 34 | return unwrap(lhs, rhs) { $0 != $1 } 35 | } 36 | 37 | /** 38 | Performs element-wise equality comparison between a DataFrame and a scalar value. 39 | Returns a DataFrame of boolean values indicating where elements are equal to the scalar. 40 | */ 41 | public func == (lhs: DataFrame?, 42 | rhs: T?) -> DataFrame? where T: Equatable { 43 | unwrap(lhs, rhs) { $0 == $1 } 44 | } 45 | 46 | /** 47 | Performs element-wise greater than or equal comparison between two DataFrames. 48 | Returns a DataFrame of boolean values indicating where left elements are >= right elements. 49 | */ 50 | public func >= (lhs: DataFrame?, 51 | rhs: DataFrame?) -> DataFrame? where T: Comparable { 52 | unwrap(lhs, rhs) { $0 >= $1 } 53 | } 54 | 55 | /** 56 | Performs element-wise greater than comparison between two DataFrames. 57 | Returns a DataFrame of boolean values indicating where left elements are > right elements. 58 | */ 59 | public func > (lhs: DataFrame?, 60 | rhs: DataFrame?) -> DataFrame? where T: Comparable { 61 | unwrap(lhs, rhs) { $0 > $1 } 62 | } 63 | 64 | /** 65 | Performs element-wise less than comparison between two DataFrames. 66 | Returns a DataFrame of boolean values indicating where left elements are < right elements. 67 | */ 68 | public func < (lhs: DataFrame?, 69 | rhs: DataFrame?) -> DataFrame? where T: Comparable { 70 | unwrap(lhs, rhs) { $0 < $1 } 71 | } 72 | 73 | /** 74 | Performs element-wise less than or equal comparison between two DataFrames. 75 | Returns a DataFrame of boolean values indicating where left elements are <= right elements. 76 | */ 77 | public func <= (lhs: DataFrame?, 78 | rhs: DataFrame?) -> DataFrame? where T: Comparable { 79 | unwrap(lhs, rhs) { $0 <= $1 } 80 | } 81 | 82 | /** 83 | Performs element-wise greater than or equal comparison between a DataFrame and a scalar. 84 | Returns a DataFrame of boolean values indicating where elements are >= the scalar. 85 | */ 86 | public func >= (lhs: DataFrame?, 87 | rhs: T?) -> DataFrame? where T: Comparable { 88 | unwrap(lhs, rhs) { $0 >= $1 } 89 | } 90 | 91 | /** 92 | Performs element-wise greater than comparison between a DataFrame and a scalar. 93 | Returns a DataFrame of boolean values indicating where elements are > the scalar. 94 | */ 95 | public func > (lhs: DataFrame?, 96 | rhs: T?) -> DataFrame? where T: Comparable { 97 | unwrap(lhs, rhs) { $0 > $1 } 98 | } 99 | 100 | /** 101 | Performs element-wise less than comparison between a DataFrame and a scalar. 102 | Returns a DataFrame of boolean values indicating where elements are < the scalar. 103 | */ 104 | public func < (lhs: DataFrame?, 105 | rhs: T?) -> DataFrame? where T: Comparable { 106 | unwrap(lhs, rhs) { $0 < $1 } 107 | } 108 | 109 | /** 110 | Performs element-wise less than or equal comparison between a DataFrame and a scalar. 111 | Returns a DataFrame of boolean values indicating where elements are <= the scalar. 112 | */ 113 | public func <= (lhs: DataFrame?, 114 | rhs: T?) -> DataFrame? where T: Comparable { 115 | unwrap(lhs, rhs) { $0 <= $1 } 116 | } 117 | 118 | /** 119 | Performs element-wise addition between two DataFrames. 120 | Returns a DataFrame with the sum of corresponding elements. 121 | */ 122 | public func + (lhs: DataFrame?, 123 | rhs: DataFrame?) -> DataFrame? where T: Numeric { 124 | unwrap(lhs, rhs) { $0 + $1 } 125 | } 126 | 127 | /** 128 | Performs element-wise subtraction between two DataFrames. 129 | Returns a DataFrame with the difference of corresponding elements. 130 | */ 131 | public func - (lhs: DataFrame?, 132 | rhs: DataFrame?) -> DataFrame? where T: Numeric { 133 | unwrap(lhs, rhs) { $0 - $1 } 134 | } 135 | 136 | /** 137 | Performs element-wise multiplication between two DataFrames. 138 | Returns a DataFrame with the product of corresponding elements. 139 | */ 140 | public func * (lhs: DataFrame?, 141 | rhs: DataFrame?) -> DataFrame? where T: Numeric { 142 | unwrap(lhs, rhs) { $0 * $1 } 143 | } 144 | 145 | /** 146 | Performs element-wise division between two DataFrames. 147 | Returns a DataFrame with the quotient of corresponding elements. 148 | */ 149 | public func / (lhs: DataFrame?, 150 | rhs: DataFrame?) -> DataFrame? where T: FloatingPoint { 151 | unwrap(lhs, rhs) { $0 / $1 } 152 | } 153 | 154 | /** 155 | Performs element-wise logical OR between two boolean DataFrames. 156 | Returns a DataFrame with the logical OR of corresponding boolean elements. 157 | */ 158 | public func || (lhs: DataFrame?, 159 | rhs: DataFrame?) -> DataFrame? { 160 | unwrap(lhs, rhs) { $0 || $1 } 161 | } 162 | 163 | /** 164 | Performs element-wise logical AND between two boolean DataFrames. 165 | Returns a DataFrame with the logical AND of corresponding boolean elements. 166 | */ 167 | public func && (lhs: DataFrame?, 168 | rhs: DataFrame?) -> DataFrame? { 169 | unwrap(lhs, rhs) { $0 && $1 } 170 | } 171 | 172 | /** 173 | Performs element-wise addition between two DataFrames. 174 | Asserts that both DataFrames have the same keys and returns the sum of corresponding elements. 175 | */ 176 | public func + (lhs: DataFrame, 177 | rhs: DataFrame) -> DataFrame { 178 | 179 | assert(Set(lhs.keys) == Set(rhs.keys), "Dataframes should have equal keys sets") 180 | 181 | var res = DataFrame() 182 | 183 | lhs.forEach { 184 | res[$0.key] = unwrap($0.value, rhs[$0.key]) { $0 + $1 } 185 | } 186 | 187 | return res 188 | } 189 | 190 | /** 191 | Performs element-wise subtraction between two DataFrames. 192 | Asserts that both DataFrames have the same keys and returns the difference of corresponding elements. 193 | */ 194 | public func - (lhs: DataFrame, 195 | rhs: DataFrame) -> DataFrame { 196 | 197 | assert(Set(lhs.keys) == Set(rhs.keys), "Dataframes should have equal keys sets") 198 | 199 | var res = DataFrame() 200 | 201 | lhs.forEach { 202 | res[$0.key] = unwrap($0.value, rhs[$0.key]) { $0 - $1 } 203 | } 204 | 205 | return res 206 | } 207 | 208 | /** 209 | Performs element-wise multiplication between two DataFrames. 210 | Asserts that both DataFrames have the same keys and returns the product of corresponding elements. 211 | */ 212 | public func * (lhs: DataFrame, 213 | rhs: DataFrame) -> DataFrame { 214 | 215 | assert(Set(lhs.keys) == Set(rhs.keys), "Dataframes should have equal keys sets") 216 | 217 | var res = DataFrame() 218 | 219 | lhs.forEach { 220 | res[$0.key] = unwrap($0.value, rhs[$0.key]) { $0 * $1 } 221 | } 222 | 223 | return res 224 | } 225 | 226 | /** 227 | Performs element-wise division between two DataFrames. 228 | Asserts that both DataFrames have the same keys and returns the quotient of corresponding elements. 229 | */ 230 | public func / (lhs: DataFrame, 231 | rhs: DataFrame) -> DataFrame { 232 | assert(Set(lhs.keys) == Set(rhs.keys), "Dataframes should have equal keys sets") 233 | 234 | var res = DataFrame() 235 | 236 | lhs.forEach { 237 | res[$0.key] = unwrap($0.value, rhs[$0.key]) { $0 / $1 } 238 | } 239 | 240 | return res 241 | } 242 | 243 | /** 244 | Performs element-wise equality comparison between two DataFrames. 245 | Asserts that both DataFrames have the same keys and returns boolean values indicating equality. 246 | */ 247 | public func == (lhs: DataFrame, 248 | rhs: DataFrame) -> DataFrame { 249 | 250 | assert(Set(lhs.keys) == Set(rhs.keys), "Dataframes should have equal keys sets") 251 | 252 | var res = DataFrame() 253 | 254 | lhs.forEach { 255 | res[$0.key] = unwrap($0.value, rhs[$0.key]) { $0 == $1 } 256 | } 257 | 258 | return res 259 | } 260 | 261 | /** 262 | Performs element-wise inequality comparison between two DataFrames. 263 | Asserts that both DataFrames have the same keys and returns boolean values indicating inequality. 264 | */ 265 | public func != (lhs: DataFrame, 266 | rhs: DataFrame) -> DataFrame { 267 | 268 | assert(Set(lhs.keys) == Set(rhs.keys), "Dataframes should have equal keys sets") 269 | 270 | var res = DataFrame() 271 | 272 | lhs.forEach { 273 | res[$0.key] = unwrap($0.value, rhs[$0.key]) { $0 != $1 } 274 | } 275 | 276 | return res 277 | } 278 | 279 | /** 280 | Performs element-wise greater than or equal comparison between two DataFrames. 281 | Asserts that both DataFrames have the same keys and returns boolean values for >= comparison. 282 | */ 283 | public func >= (lhs: DataFrame, 284 | rhs: DataFrame) -> DataFrame where T: Comparable { 285 | 286 | assert(Set(lhs.keys) == Set(rhs.keys), "Dataframes should have equal keys sets") 287 | 288 | var res = DataFrame() 289 | 290 | lhs.forEach { 291 | res[$0.key] = unwrap($0.value, rhs[$0.key]) { $0 >= $1 } 292 | } 293 | 294 | return res 295 | } 296 | 297 | /** 298 | Performs element-wise greater than comparison between two DataFrames. 299 | Asserts that both DataFrames have the same keys and returns boolean values for > comparison. 300 | */ 301 | public func > (lhs: DataFrame, 302 | rhs: DataFrame) -> DataFrame where T: Comparable { 303 | 304 | assert(Set(lhs.keys) == Set(rhs.keys), "Dataframes should have equal keys sets") 305 | 306 | var res = DataFrame() 307 | 308 | lhs.forEach { 309 | res[$0.key] = unwrap($0.value, rhs[$0.key]) { $0 > $1 } 310 | } 311 | 312 | return res 313 | } 314 | 315 | /** 316 | Performs element-wise less than or equal comparison between two DataFrames. 317 | Asserts that both DataFrames have the same keys and returns boolean values for <= comparison. 318 | */ 319 | public func <= (lhs: DataFrame, 320 | rhs: DataFrame) -> DataFrame where T: Comparable { 321 | 322 | assert(Set(lhs.keys) == Set(rhs.keys), "Dataframes should have equal keys sets") 323 | 324 | var res = DataFrame() 325 | 326 | lhs.forEach { 327 | res[$0.key] = unwrap($0.value, rhs[$0.key]) { $0 <= $1 } 328 | } 329 | 330 | return res 331 | } 332 | 333 | /** 334 | Performs element-wise less than comparison between two DataFrames. 335 | Asserts that both DataFrames have the same keys and returns boolean values for < comparison. 336 | */ 337 | public func < (lhs: DataFrame, 338 | rhs: DataFrame) -> DataFrame where T: Comparable { 339 | 340 | assert(Set(lhs.keys) == Set(rhs.keys), "Dataframes should have equal keys sets") 341 | 342 | var res = DataFrame() 343 | 344 | lhs.forEach { 345 | res[$0.key] = unwrap($0.value, rhs[$0.key]) { $0 < $1 } 346 | } 347 | 348 | return res 349 | } 350 | 351 | /** 352 | Performs element-wise less than comparison between a DataFrame and a scalar value. 353 | Converts the scalar to a DataFrame and performs the comparison. 354 | */ 355 | public func < (lhs: DataFrame, 356 | rhs: T) -> DataFrame where T: Comparable { 357 | 358 | let rhsConst = lhs.mapTo(constant: rhs) 359 | return lhs < rhsConst 360 | } 361 | 362 | /** 363 | Performs element-wise less than or equal comparison between a DataFrame and a scalar value. 364 | Converts the scalar to a DataFrame and performs the comparison. 365 | */ 366 | public func <= (lhs: DataFrame, 367 | rhs: T) -> DataFrame where T: Comparable { 368 | 369 | let rhsConst = lhs.mapTo(constant: rhs) 370 | return lhs <= rhsConst 371 | } 372 | 373 | /** 374 | Performs element-wise greater than comparison between a DataFrame and a scalar value. 375 | Converts the scalar to a DataFrame and performs the comparison. 376 | */ 377 | public func > (lhs: DataFrame, 378 | rhs: T) -> DataFrame where T: Comparable { 379 | 380 | let rhsConst = lhs.mapTo(constant: rhs) 381 | return lhs > rhsConst 382 | } 383 | 384 | /** 385 | Performs element-wise greater than or equal comparison between a DataFrame and a scalar value. 386 | Converts the scalar to a DataFrame and performs the comparison. 387 | */ 388 | public func >= (lhs: DataFrame, 389 | rhs: T) -> DataFrame where T: Comparable { 390 | 391 | let rhsConst = lhs.mapTo(constant: rhs) 392 | return lhs >= rhsConst 393 | } 394 | 395 | /** 396 | Performs element-wise equality comparison between a DataFrame and a scalar value. 397 | Converts the scalar to a DataFrame and performs the comparison. 398 | */ 399 | public func == (lhs: DataFrame, 400 | rhs: T) -> DataFrame where T: Equatable { 401 | 402 | let rhsConst = lhs.mapTo(constant: rhs) 403 | return lhs == rhsConst 404 | } 405 | 406 | /** 407 | Performs element-wise inequality comparison between a DataFrame and a scalar value. 408 | Converts the scalar to a DataFrame and performs the comparison. 409 | */ 410 | public func != (lhs: DataFrame, 411 | rhs: T) -> DataFrame where T: Equatable { 412 | 413 | let rhsConst = lhs.mapTo(constant: rhs) 414 | return lhs != rhsConst 415 | } 416 | 417 | /** 418 | Performs element-wise logical AND between two boolean DataFrames. 419 | Asserts that both DataFrames have the same keys and returns boolean values for logical AND. 420 | */ 421 | public func && (lhs: DataFrame, 422 | rhs: DataFrame) -> DataFrame { 423 | 424 | assert(Set(lhs.keys) == Set(rhs.keys), "Dataframes should have equal keys sets") 425 | 426 | var res = DataFrame() 427 | 428 | lhs.forEach { 429 | res[$0.key] = unwrap($0.value, rhs[$0.key]) { $0 && $1 } 430 | } 431 | 432 | return res 433 | } 434 | 435 | /** 436 | Performs element-wise logical OR between two boolean DataFrames. 437 | Asserts that both DataFrames have the same keys and returns boolean values for logical OR. 438 | */ 439 | public func || (lhs: DataFrame, 440 | rhs: DataFrame) -> DataFrame { 441 | 442 | assert(Set(lhs.keys) == Set(rhs.keys), "Dataframes should have equal keys sets") 443 | 444 | var res = DataFrame() 445 | 446 | lhs.forEach { 447 | res[$0.key] = unwrap($0.value, rhs[$0.key]) { $0 || $1 } 448 | } 449 | 450 | return res 451 | } 452 | -------------------------------------------------------------------------------- /Tests/KoalasTests/UtilityTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UtilityTests.swift 3 | // 4 | // 5 | // Created by AI Assistant on 2024. 6 | // 7 | 8 | import XCTest 9 | @testable import Koalas 10 | 11 | final class UtilityTests: XCTestCase { 12 | 13 | // MARK: - UnwrapUtils Tests 14 | 15 | func test_unwrap_TwoValues() { 16 | let result = unwrap(5, 10) { a, b in 17 | a + b 18 | } 19 | 20 | XCTAssertEqual(result, 15) 21 | } 22 | 23 | func test_unwrap_TwoValuesWithNil() { 24 | let result = unwrap(5, nil) { a, b in 25 | a + b 26 | } 27 | 28 | XCTAssertNil(result) 29 | } 30 | 31 | func test_unwrap_TwoValuesBothNil() { 32 | let result = unwrap(nil as Int?, nil as Int?) { a, b in 33 | a + b 34 | } 35 | 36 | XCTAssertNil(result) 37 | } 38 | 39 | func test_unwrap_TwoDifferentTypes() { 40 | let result = unwrap(5, "hello") { a, b in 41 | "\(a) + \(b)" 42 | } 43 | 44 | XCTAssertEqual(result, "5 + hello") 45 | } 46 | 47 | func test_unwrap_TwoDifferentTypesWithNil() { 48 | let result = unwrap(5, nil as String?) { a, b in 49 | "\(a) + \(b)" 50 | } 51 | 52 | XCTAssertNil(result) 53 | } 54 | 55 | func test_unwrap_ThreeValues() { 56 | let result = unwrap(1, 2, 3) { a, b, c in 57 | a + b + c 58 | } 59 | 60 | XCTAssertEqual(result, 6) 61 | } 62 | 63 | func test_unwrap_ThreeValuesWithNil() { 64 | let result = unwrap(1, nil, 3) { a, b, c in 65 | a + b + c 66 | } 67 | 68 | XCTAssertNil(result) 69 | } 70 | 71 | func test_unwrap_SingleValue() { 72 | let result = unwrap(value: 5) { a in 73 | a * 2 74 | } 75 | 76 | XCTAssertEqual(result, 10) 77 | } 78 | 79 | func test_unwrap_SingleValueWithNil() { 80 | let result = unwrap(value: nil as Int?) { a in 81 | a * 2 82 | } 83 | 84 | XCTAssertNil(result) 85 | } 86 | 87 | // MARK: - DataSeriesType Tests 88 | 89 | func test_toDataSeriesWithShape_WithDataSeries() { 90 | let referenceSeries = DataSeries([1, 2, 3, 4]) 91 | let dataSeries = DataSeries([10, 20, 30, 40]) 92 | 93 | let dataSeriesType: DataSeriesType, Int> = .ds(dataSeries) 94 | 95 | let result = dataSeriesType.toDataSeriesWithShape(of: referenceSeries) 96 | 97 | XCTAssertNotNil(result) 98 | XCTAssertEqual(result?.count, 4) 99 | XCTAssertEqual(result?[0], 10) 100 | XCTAssertEqual(result?[1], 20) 101 | XCTAssertEqual(result?[2], 30) 102 | XCTAssertEqual(result?[3], 40) 103 | } 104 | 105 | func test_toDataSeriesWithShape_WithScalarValue() { 106 | let referenceSeries = DataSeries([1, 2, 3, 4]) 107 | let scalarValue = 100 108 | 109 | let dataSeriesType: DataSeriesType, Int> = .value(scalarValue) 110 | 111 | let result = dataSeriesType.toDataSeriesWithShape(of: referenceSeries) 112 | 113 | XCTAssertNotNil(result) 114 | XCTAssertEqual(result?.count, 4) 115 | XCTAssertEqual(result?[0], 100) 116 | XCTAssertEqual(result?[1], 100) 117 | XCTAssertEqual(result?[2], 100) 118 | XCTAssertEqual(result?[3], 100) 119 | } 120 | 121 | func test_toDataSeriesWithShape_WithNilDataSeries() { 122 | let referenceSeries = DataSeries([1, 2, 3, 4]) 123 | 124 | let dataSeriesType: DataSeriesType, Int> = .ds(nil) 125 | 126 | let result = dataSeriesType.toDataSeriesWithShape(of: referenceSeries) 127 | 128 | XCTAssertNil(result) 129 | } 130 | 131 | func test_toDataSeriesWithShape_WithNilScalarValue() { 132 | let referenceSeries = DataSeries([1, 2, 3, 4]) 133 | 134 | let dataSeriesType: DataSeriesType, Int> = .value(nil) 135 | 136 | let result = dataSeriesType.toDataSeriesWithShape(of: referenceSeries) 137 | 138 | XCTAssertNotNil(result) 139 | XCTAssertEqual(result?.count, 4) 140 | XCTAssertEqual(result?[0], nil) 141 | XCTAssertEqual(result?[1], nil) 142 | XCTAssertEqual(result?[2], nil) 143 | XCTAssertEqual(result?[3], nil) 144 | } 145 | 146 | // MARK: - DataFrameType Tests 147 | 148 | func test_toDataframeWithShape_WithDataFrame() { 149 | let referenceDataFrame = DataFrame(dictionaryLiteral: 150 | ("col1", DataSeries([1, 2, 3])), 151 | ("col2", DataSeries([4, 5, 6])) 152 | ) 153 | 154 | let dataFrame = DataFrame(dictionaryLiteral: 155 | ("col1", DataSeries([10, 20, 30])), 156 | ("col2", DataSeries([40, 50, 60])) 157 | ) 158 | 159 | let dataFrameType: DataFrameType, Int> = .df(dataFrame) 160 | 161 | let result = dataFrameType.toDataframeWithShape(of: referenceDataFrame) 162 | 163 | XCTAssertNotNil(result) 164 | XCTAssertEqual(result?["col1"]?.count, 3) 165 | XCTAssertEqual(result?["col2"]?.count, 3) 166 | XCTAssertEqual(result?["col1"]?[0], 10) 167 | XCTAssertEqual(result?["col2"]?[0], 40) 168 | } 169 | 170 | func test_toDataframeWithShape_WithScalarValue() { 171 | let referenceDataFrame = DataFrame(dictionaryLiteral: 172 | ("col1", DataSeries([1, 2, 3])), 173 | ("col2", DataSeries([4, 5, 6])) 174 | ) 175 | 176 | let scalarValue = 100 177 | 178 | let dataFrameType: DataFrameType, Int> = .value(scalarValue) 179 | 180 | let result = dataFrameType.toDataframeWithShape(of: referenceDataFrame) 181 | 182 | XCTAssertNotNil(result) 183 | XCTAssertEqual(result?["col1"]?.count, 3) 184 | XCTAssertEqual(result?["col2"]?.count, 3) 185 | XCTAssertEqual(result?["col1"]?[0], 100) 186 | XCTAssertEqual(result?["col1"]?[1], 100) 187 | XCTAssertEqual(result?["col1"]?[2], 100) 188 | XCTAssertEqual(result?["col2"]?[0], 100) 189 | XCTAssertEqual(result?["col2"]?[1], 100) 190 | XCTAssertEqual(result?["col2"]?[2], 100) 191 | } 192 | 193 | func test_toDataframeWithShape_WithNilDataFrame() { 194 | let referenceDataFrame = DataFrame(dictionaryLiteral: 195 | ("col1", DataSeries([1, 2, 3])), 196 | ("col2", DataSeries([4, 5, 6])) 197 | ) 198 | 199 | let dataFrameType: DataFrameType, Int> = .df(nil) 200 | 201 | let result = dataFrameType.toDataframeWithShape(of: referenceDataFrame) 202 | 203 | XCTAssertNil(result) 204 | } 205 | 206 | func test_toDataframeWithShape_WithNilScalarValue() { 207 | let referenceDataFrame = DataFrame(dictionaryLiteral: 208 | ("col1", DataSeries([1, 2, 3])), 209 | ("col2", DataSeries([4, 5, 6])) 210 | ) 211 | 212 | let dataFrameType: DataFrameType, Int> = .value(nil) 213 | 214 | let result = dataFrameType.toDataframeWithShape(of: referenceDataFrame) 215 | 216 | XCTAssertNotNil(result) 217 | XCTAssertEqual(result?["col1"]?.count, 3) 218 | XCTAssertEqual(result?["col2"]?.count, 3) 219 | XCTAssertEqual(result?["col1"]?[0], nil) 220 | XCTAssertEqual(result?["col1"]?[1], nil) 221 | XCTAssertEqual(result?["col1"]?[2], nil) 222 | XCTAssertEqual(result?["col2"]?[0], nil) 223 | XCTAssertEqual(result?["col2"]?[1], nil) 224 | XCTAssertEqual(result?["col2"]?[2], nil) 225 | } 226 | 227 | // MARK: - Tuple Tests 228 | 229 | func test_Tuple2_Initialization() { 230 | let tuple = Tuple2(t1: 1, t2: "hello") 231 | 232 | XCTAssertEqual(tuple.t1, 1) 233 | XCTAssertEqual(tuple.t2, "hello") 234 | } 235 | 236 | func test_Tuple3_Initialization() { 237 | let tuple = Tuple3(t1: 1, t2: "hello", t3: 3.14) 238 | 239 | XCTAssertEqual(tuple.t1, 1) 240 | XCTAssertEqual(tuple.t2, "hello") 241 | XCTAssertEqual(tuple.t3, 3.14) 242 | } 243 | 244 | func test_Tuple4_Initialization() { 245 | let tuple = Tuple4(t1: 1, t2: "hello", t3: 3.14, t4: true) 246 | 247 | XCTAssertEqual(tuple.t1, 1) 248 | XCTAssertEqual(tuple.t2, "hello") 249 | XCTAssertEqual(tuple.t3, 3.14) 250 | XCTAssertEqual(tuple.t4, true) 251 | } 252 | 253 | func test_Tuple_Codable() { 254 | let tuple = Tuple2(t1: 1, t2: "hello") 255 | 256 | // Test that it can be encoded and decoded 257 | let encoder = JSONEncoder() 258 | let decoder = JSONDecoder() 259 | 260 | do { 261 | let data = try encoder.encode(tuple) 262 | let decoded = try decoder.decode(Tuple2.self, from: data) 263 | 264 | XCTAssertEqual(decoded.t1, 1) 265 | XCTAssertEqual(decoded.t2, "hello") 266 | } catch { 267 | XCTFail("Failed to encode/decode tuple: \(error)") 268 | } 269 | } 270 | 271 | // MARK: - FillNilsMethod Tests 272 | 273 | func test_FillNilsMethod_All() { 274 | let method = FillNilsMethod.all(value: 0) 275 | 276 | if case .all(let value) = method { 277 | XCTAssertEqual(value, 0) 278 | } else { 279 | XCTFail("Expected .all case") 280 | } 281 | } 282 | 283 | func test_FillNilsMethod_Backward() { 284 | let method = FillNilsMethod.backward(initial: 10) 285 | 286 | if case .backward(let initial) = method { 287 | XCTAssertEqual(initial, 10) 288 | } else { 289 | XCTFail("Expected .backward case") 290 | } 291 | } 292 | 293 | func test_FillNilsMethod_Forward() { 294 | let method = FillNilsMethod.forward(initial: 20) 295 | 296 | if case .forward(let initial) = method { 297 | XCTAssertEqual(initial, 20) 298 | } else { 299 | XCTFail("Expected .forward case") 300 | } 301 | } 302 | 303 | // MARK: - DateComponentsKeys Tests 304 | 305 | func test_DateComponentsKeys_Values() { 306 | XCTAssertEqual(DateComponentsKeys.year.rawValue, "year") 307 | XCTAssertEqual(DateComponentsKeys.month.rawValue, "month") 308 | XCTAssertEqual(DateComponentsKeys.day.rawValue, "day") 309 | } 310 | 311 | // MARK: - SeriesArray Core Function Tests 312 | 313 | func test_SeriesArray_IndexAfter() { 314 | let array = SeriesArray([1, 2, 3, 4, 5]) 315 | 316 | XCTAssertEqual(array.index(after: 0), 1) 317 | XCTAssertEqual(array.index(after: 2), 3) 318 | XCTAssertEqual(array.index(after: 4), 5) 319 | } 320 | 321 | func test_SeriesArray_Append() { 322 | var array = SeriesArray([1, 2, 3]) 323 | 324 | array.append(4) 325 | 326 | XCTAssertEqual(array.count, 4) 327 | XCTAssertEqual(array[3], 4) 328 | } 329 | 330 | func test_SeriesArray_AppendContentsOf() { 331 | var array = SeriesArray([1, 2, 3]) 332 | 333 | array.append(contentsOf: [4, 5, 6]) 334 | 335 | XCTAssertEqual(array.count, 6) 336 | XCTAssertEqual(array[3], 4) 337 | XCTAssertEqual(array[4], 5) 338 | XCTAssertEqual(array[5], 6) 339 | } 340 | 341 | func test_SeriesArray_Filter() { 342 | let array = SeriesArray([1, 2, 3, 4, 5, 6]) 343 | 344 | let filtered = array.filter { $0 % 2 == 0 } 345 | 346 | XCTAssertEqual(filtered.count, 3) 347 | XCTAssertEqual(filtered[0], 2) 348 | XCTAssertEqual(filtered[1], 4) 349 | XCTAssertEqual(filtered[2], 6) 350 | } 351 | 352 | func test_SeriesArray_Insert() { 353 | var array = SeriesArray([1, 2, 3]) 354 | 355 | array.insert(10, at: 1) 356 | 357 | XCTAssertEqual(array.count, 4) 358 | XCTAssertEqual(array[0], 1) 359 | XCTAssertEqual(array[1], 10) 360 | XCTAssertEqual(array[2], 2) 361 | XCTAssertEqual(array[3], 3) 362 | } 363 | 364 | func test_SeriesArray_InsertContentsOf() { 365 | var array = SeriesArray([1, 2, 3]) 366 | 367 | array.insert(contentsOf: [10, 20], at: 1) 368 | 369 | XCTAssertEqual(array.count, 5) 370 | XCTAssertEqual(array[0], 1) 371 | XCTAssertEqual(array[1], 10) 372 | XCTAssertEqual(array[2], 20) 373 | XCTAssertEqual(array[3], 2) 374 | XCTAssertEqual(array[4], 3) 375 | } 376 | 377 | func test_SeriesArray_PopLast() { 378 | var array = SeriesArray([1, 2, 3]) 379 | 380 | let last = array.popLast() 381 | 382 | XCTAssertEqual(last, 3) 383 | XCTAssertEqual(array.count, 2) 384 | } 385 | 386 | func test_SeriesArray_RemoveAt() { 387 | var array = SeriesArray([1, 2, 3, 4]) 388 | 389 | let removed = array.remove(at: 1) 390 | 391 | XCTAssertEqual(removed, 2) 392 | XCTAssertEqual(array.count, 3) 393 | XCTAssertEqual(array[0], 1) 394 | XCTAssertEqual(array[1], 3) 395 | XCTAssertEqual(array[2], 4) 396 | } 397 | 398 | func test_SeriesArray_RemoveAll() { 399 | var array = SeriesArray([1, 2, 3, 4]) 400 | 401 | array.removeAll(keepingCapacity: false) 402 | 403 | XCTAssertEqual(array.count, 0) 404 | } 405 | 406 | func test_SeriesArray_RemoveAllWhere() { 407 | var array = SeriesArray([1, 2, 3, 4, 5, 6]) 408 | 409 | array.removeAll { $0 % 2 == 0 } 410 | 411 | XCTAssertEqual(array.count, 3) 412 | XCTAssertEqual(array[0], 1) 413 | XCTAssertEqual(array[1], 3) 414 | XCTAssertEqual(array[2], 5) 415 | } 416 | 417 | func test_SeriesArray_RemoveFirst() { 418 | var array = SeriesArray([1, 2, 3, 4]) 419 | 420 | let first = array.removeFirst() 421 | 422 | XCTAssertEqual(first, 1) 423 | XCTAssertEqual(array.count, 3) 424 | XCTAssertEqual(array[0], 2) 425 | } 426 | 427 | func test_SeriesArray_RemoveFirstK() { 428 | var array = SeriesArray([1, 2, 3, 4, 5]) 429 | 430 | array.removeFirst(2) 431 | 432 | XCTAssertEqual(array.count, 3) 433 | XCTAssertEqual(array[0], 3) 434 | XCTAssertEqual(array[1], 4) 435 | XCTAssertEqual(array[2], 5) 436 | } 437 | 438 | func test_SeriesArray_RemoveLast() { 439 | var array = SeriesArray([1, 2, 3, 4]) 440 | 441 | let last = array.removeLast() 442 | 443 | XCTAssertEqual(last, 4) 444 | XCTAssertEqual(array.count, 3) 445 | XCTAssertEqual(array[2], 3) 446 | } 447 | 448 | func test_SeriesArray_RemoveLastK() { 449 | var array = SeriesArray([1, 2, 3, 4, 5]) 450 | 451 | array.removeLast(2) 452 | 453 | XCTAssertEqual(array.count, 3) 454 | XCTAssertEqual(array[0], 1) 455 | XCTAssertEqual(array[1], 2) 456 | XCTAssertEqual(array[2], 3) 457 | } 458 | 459 | func test_SeriesArray_RemoveSubrange() { 460 | var array = SeriesArray([1, 2, 3, 4, 5]) 461 | 462 | array.removeSubrange(1..<4) 463 | 464 | XCTAssertEqual(array.count, 2) 465 | XCTAssertEqual(array[0], 1) 466 | XCTAssertEqual(array[1], 5) 467 | } 468 | 469 | func test_SeriesArray_ReplaceSubrange() { 470 | var array = SeriesArray([1, 2, 3, 4, 5]) 471 | 472 | array.replaceSubrange(1..<4, with: [10, 20, 30]) 473 | 474 | XCTAssertEqual(array.count, 5) 475 | XCTAssertEqual(array[0], 1) 476 | XCTAssertEqual(array[1], 10) 477 | XCTAssertEqual(array[2], 20) 478 | XCTAssertEqual(array[3], 30) 479 | XCTAssertEqual(array[4], 5) 480 | } 481 | 482 | func test_SeriesArray_ReserveCapacity() { 483 | var array = SeriesArray([1, 2, 3]) 484 | 485 | array.reserveCapacity(100) 486 | 487 | // Capacity is implementation detail, but we can test it doesn't crash 488 | XCTAssertEqual(array.count, 3) 489 | } 490 | 491 | func test_SeriesArray_SubscriptRange() { 492 | let array = SeriesArray([1, 2, 3, 4, 5]) 493 | 494 | let subArray = array[1..<4] 495 | 496 | XCTAssertEqual(subArray.count, 3) 497 | XCTAssertEqual(subArray[0], 2) 498 | XCTAssertEqual(subArray[1], 3) 499 | XCTAssertEqual(subArray[2], 4) 500 | } 501 | 502 | func test_SeriesArray_SubscriptSingle() { 503 | var array = SeriesArray([1, 2, 3, 4, 5]) 504 | 505 | XCTAssertEqual(array[2], 3) 506 | 507 | array[2] = 100 508 | 509 | XCTAssertEqual(array[2], 100) 510 | } 511 | 512 | func test_SeriesArray_Description() { 513 | let array = SeriesArray([1, 2, 3]) 514 | 515 | XCTAssertEqual(array.description, "[1, 2, 3]") 516 | } 517 | } -------------------------------------------------------------------------------- /Tests/KoalasTests/ArithmeticOperatorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArithmeticOperatorTests.swift 3 | // 4 | // 5 | // Created by AI Assistant on 2024. 6 | // 7 | 8 | import XCTest 9 | @testable import Koalas 10 | 11 | final class ArithmeticOperatorTests: XCTestCase { 12 | 13 | // MARK: - DataFrame Arithmetic Operators with Optional DataFrames 14 | 15 | func test_DataFrameAddition_WithOptionalDataFrames() { 16 | let s1 = DataSeries([1, 2, 3]) 17 | let s2 = DataSeries([4, 5, 6]) 18 | let s3 = DataSeries([7, 8, 9]) 19 | let s4 = DataSeries([10, 11, 12]) 20 | 21 | let df1: DataFrame? = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 22 | let df2: DataFrame? = DataFrame(dictionaryLiteral: ("col1", s3), ("col2", s4)) 23 | 24 | let result = df1 + df2 25 | 26 | XCTAssertNotNil(result) 27 | XCTAssertEqual(result?["col1"]?[0], 8) // 1 + 7 28 | XCTAssertEqual(result?["col1"]?[1], 10) // 2 + 8 29 | XCTAssertEqual(result?["col1"]?[2], 12) // 3 + 9 30 | XCTAssertEqual(result?["col2"]?[0], 14) // 4 + 10 31 | XCTAssertEqual(result?["col2"]?[1], 16) // 5 + 11 32 | XCTAssertEqual(result?["col2"]?[2], 18) // 6 + 12 33 | } 34 | 35 | func test_DataFrameAddition_WithNilDataFrames() { 36 | let s1 = DataSeries([1, 2, 3]) 37 | let s2 = DataSeries([4, 5, 6]) 38 | 39 | let df1: DataFrame? = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 40 | let df2: DataFrame? = nil 41 | 42 | let result = df1 + df2 43 | 44 | XCTAssertNil(result) 45 | } 46 | 47 | func test_DataFrameSubtraction_WithOptionalDataFrames() { 48 | let s1 = DataSeries([10, 20, 30]) 49 | let s2 = DataSeries([40, 50, 60]) 50 | let s3 = DataSeries([1, 2, 3]) 51 | let s4 = DataSeries([4, 5, 6]) 52 | 53 | let df1: DataFrame? = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 54 | let df2: DataFrame? = DataFrame(dictionaryLiteral: ("col1", s3), ("col2", s4)) 55 | 56 | let result = df1 - df2 57 | 58 | XCTAssertNotNil(result) 59 | XCTAssertEqual(result?["col1"]?[0], 9) // 10 - 1 60 | XCTAssertEqual(result?["col1"]?[1], 18) // 20 - 2 61 | XCTAssertEqual(result?["col1"]?[2], 27) // 30 - 3 62 | XCTAssertEqual(result?["col2"]?[0], 36) // 40 - 4 63 | XCTAssertEqual(result?["col2"]?[1], 45) // 50 - 5 64 | XCTAssertEqual(result?["col2"]?[2], 54) // 60 - 6 65 | } 66 | 67 | func test_DataFrameMultiplication_WithOptionalDataFrames() { 68 | let s1 = DataSeries([2, 3, 4]) 69 | let s2 = DataSeries([5, 6, 7]) 70 | let s3 = DataSeries([3, 4, 5]) 71 | let s4 = DataSeries([2, 3, 4]) 72 | 73 | let df1: DataFrame? = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 74 | let df2: DataFrame? = DataFrame(dictionaryLiteral: ("col1", s3), ("col2", s4)) 75 | 76 | let result = df1 * df2 77 | 78 | XCTAssertNotNil(result) 79 | XCTAssertEqual(result?["col1"]?[0], 6) // 2 * 3 80 | XCTAssertEqual(result?["col1"]?[1], 12) // 3 * 4 81 | XCTAssertEqual(result?["col1"]?[2], 20) // 4 * 5 82 | XCTAssertEqual(result?["col2"]?[0], 10) // 5 * 2 83 | XCTAssertEqual(result?["col2"]?[1], 18) // 6 * 3 84 | XCTAssertEqual(result?["col2"]?[2], 28) // 7 * 4 85 | } 86 | 87 | func test_DataFrameDivision_WithOptionalDataFrames() { 88 | let s1 = DataSeries([10.0, 20.0, 30.0]) 89 | let s2 = DataSeries([40.0, 50.0, 60.0]) 90 | let s3 = DataSeries([2.0, 4.0, 5.0]) 91 | let s4 = DataSeries([5.0, 10.0, 12.0]) 92 | 93 | let df1: DataFrame? = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 94 | let df2: DataFrame? = DataFrame(dictionaryLiteral: ("col1", s3), ("col2", s4)) 95 | 96 | let result = df1 / df2 97 | 98 | XCTAssertNotNil(result) 99 | XCTAssertEqual(result?["col1"]?[0], 5.0) // 10 / 2 100 | XCTAssertEqual(result?["col1"]?[1], 5.0) // 20 / 4 101 | XCTAssertEqual(result?["col1"]?[2], 6.0) // 30 / 5 102 | XCTAssertEqual(result?["col2"]?[0], 8.0) // 40 / 5 103 | XCTAssertEqual(result?["col2"]?[1], 5.0) // 50 / 10 104 | XCTAssertEqual(result?["col2"]?[2], 5.0) // 60 / 12 105 | } 106 | 107 | // MARK: - DataFrame Comparison Operators with Optional DataFrames 108 | 109 | func test_DataFrameEquality_WithOptionalDataFrames() { 110 | let s1 = DataSeries([1, 2, 3]) 111 | let s2 = DataSeries([4, 5, 6]) 112 | let s3 = DataSeries([1, 2, 3]) 113 | let s4 = DataSeries([4, 5, 6]) 114 | 115 | let df1: DataFrame? = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 116 | let df2: DataFrame? = DataFrame(dictionaryLiteral: ("col1", s3), ("col2", s4)) 117 | 118 | let result = df1 == df2 119 | 120 | XCTAssertNotNil(result) 121 | XCTAssertEqual(result?["col1"]?[0], true) // 1 == 1 122 | XCTAssertEqual(result?["col1"]?[1], true) // 2 == 2 123 | XCTAssertEqual(result?["col1"]?[2], true) // 3 == 3 124 | XCTAssertEqual(result?["col2"]?[0], true) // 4 == 4 125 | XCTAssertEqual(result?["col2"]?[1], true) // 5 == 5 126 | XCTAssertEqual(result?["col2"]?[2], true) // 6 == 6 127 | } 128 | 129 | func test_DataFrameInequality_WithOptionalDataFrames() { 130 | let s1 = DataSeries([1, 2, 3]) 131 | let s2 = DataSeries([4, 5, 6]) 132 | let s3 = DataSeries([2, 3, 4]) 133 | let s4 = DataSeries([5, 6, 7]) 134 | 135 | let df1: DataFrame? = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 136 | let df2: DataFrame? = DataFrame(dictionaryLiteral: ("col1", s3), ("col2", s4)) 137 | 138 | let result = df1 != df2 139 | 140 | XCTAssertNotNil(result) 141 | XCTAssertEqual(result?["col1"]?[0], true) // 1 != 2 142 | XCTAssertEqual(result?["col1"]?[1], true) // 2 != 3 143 | XCTAssertEqual(result?["col1"]?[2], true) // 3 != 4 144 | XCTAssertEqual(result?["col2"]?[0], true) // 4 != 5 145 | XCTAssertEqual(result?["col2"]?[1], true) // 5 != 6 146 | XCTAssertEqual(result?["col2"]?[2], true) // 6 != 7 147 | } 148 | 149 | func test_DataFrameLessThan_WithOptionalDataFrames() { 150 | let s1 = DataSeries([1, 2, 3]) 151 | let s2 = DataSeries([4, 5, 6]) 152 | let s3 = DataSeries([2, 3, 4]) 153 | let s4 = DataSeries([5, 6, 7]) 154 | 155 | let df1: DataFrame? = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 156 | let df2: DataFrame? = DataFrame(dictionaryLiteral: ("col1", s3), ("col2", s4)) 157 | 158 | let result = df1 < df2 159 | 160 | XCTAssertNotNil(result) 161 | XCTAssertEqual(result?["col1"]?[0], true) // 1 < 2 162 | XCTAssertEqual(result?["col1"]?[1], true) // 2 < 3 163 | XCTAssertEqual(result?["col1"]?[2], true) // 3 < 4 164 | XCTAssertEqual(result?["col2"]?[0], true) // 4 < 5 165 | XCTAssertEqual(result?["col2"]?[1], true) // 5 < 6 166 | XCTAssertEqual(result?["col2"]?[2], true) // 6 < 7 167 | } 168 | 169 | func test_DataFrameGreaterThan_WithOptionalDataFrames() { 170 | let s1 = DataSeries([3, 4, 5]) 171 | let s2 = DataSeries([6, 7, 8]) 172 | let s3 = DataSeries([1, 2, 3]) 173 | let s4 = DataSeries([4, 5, 6]) 174 | 175 | let df1: DataFrame? = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 176 | let df2: DataFrame? = DataFrame(dictionaryLiteral: ("col1", s3), ("col2", s4)) 177 | 178 | let result = df1 > df2 179 | 180 | XCTAssertNotNil(result) 181 | XCTAssertEqual(result?["col1"]?[0], true) // 3 > 1 182 | XCTAssertEqual(result?["col1"]?[1], true) // 4 > 2 183 | XCTAssertEqual(result?["col1"]?[2], true) // 5 > 3 184 | XCTAssertEqual(result?["col2"]?[0], true) // 6 > 4 185 | XCTAssertEqual(result?["col2"]?[1], true) // 7 > 5 186 | XCTAssertEqual(result?["col2"]?[2], true) // 8 > 6 187 | } 188 | 189 | // MARK: - DataFrame Comparison with Scalars 190 | 191 | func test_DataFrameEquality_WithScalar() { 192 | let s1 = DataSeries([1, 2, 3]) 193 | let s2 = DataSeries([4, 5, 6]) 194 | 195 | let df: DataFrame? = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 196 | let scalar = 2 197 | 198 | let result = df == scalar 199 | 200 | XCTAssertNotNil(result) 201 | XCTAssertEqual(result?["col1"]?[0], false) // 1 == 2 202 | XCTAssertEqual(result?["col1"]?[1], true) // 2 == 2 203 | XCTAssertEqual(result?["col1"]?[2], false) // 3 == 2 204 | XCTAssertEqual(result?["col2"]?[0], false) // 4 == 2 205 | XCTAssertEqual(result?["col2"]?[1], false) // 5 == 2 206 | XCTAssertEqual(result?["col2"]?[2], false) // 6 == 2 207 | } 208 | 209 | func test_DataFrameInequality_WithScalar() { 210 | let s1 = DataSeries([1, 2, 3]) 211 | let s2 = DataSeries([4, 5, 6]) 212 | 213 | let df: DataFrame? = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 214 | let scalar = 2 215 | 216 | let result = df != scalar 217 | 218 | XCTAssertNotNil(result) 219 | XCTAssertEqual(result?["col1"]?[0], true) // 1 != 2 220 | XCTAssertEqual(result?["col1"]?[1], false) // 2 != 2 221 | XCTAssertEqual(result?["col1"]?[2], true) // 3 != 2 222 | XCTAssertEqual(result?["col2"]?[0], true) // 4 != 2 223 | XCTAssertEqual(result?["col2"]?[1], true) // 5 != 2 224 | XCTAssertEqual(result?["col2"]?[2], true) // 6 != 2 225 | } 226 | 227 | func test_DataFrameLessThan_WithScalar() { 228 | let s1 = DataSeries([1, 2, 3]) 229 | let s2 = DataSeries([4, 5, 6]) 230 | 231 | let df: DataFrame? = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 232 | let scalar = 3 233 | 234 | let result = df < scalar 235 | 236 | XCTAssertNotNil(result) 237 | XCTAssertEqual(result?["col1"]?[0], true) // 1 < 3 238 | XCTAssertEqual(result?["col1"]?[1], true) // 2 < 3 239 | XCTAssertEqual(result?["col1"]?[2], false) // 3 < 3 240 | XCTAssertEqual(result?["col2"]?[0], false) // 4 < 3 241 | XCTAssertEqual(result?["col2"]?[1], false) // 5 < 3 242 | XCTAssertEqual(result?["col2"]?[2], false) // 6 < 3 243 | } 244 | 245 | func test_DataFrameGreaterThan_WithScalar() { 246 | let s1 = DataSeries([1, 2, 3]) 247 | let s2 = DataSeries([4, 5, 6]) 248 | 249 | let df: DataFrame? = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 250 | let scalar = 3 251 | 252 | let result = df > scalar 253 | 254 | XCTAssertNotNil(result) 255 | XCTAssertEqual(result?["col1"]?[0], false) // 1 > 3 256 | XCTAssertEqual(result?["col1"]?[1], false) // 2 > 3 257 | XCTAssertEqual(result?["col1"]?[2], false) // 3 > 3 258 | XCTAssertEqual(result?["col2"]?[0], true) // 4 > 3 259 | XCTAssertEqual(result?["col2"]?[1], true) // 5 > 3 260 | XCTAssertEqual(result?["col2"]?[2], true) // 6 > 3 261 | } 262 | 263 | // MARK: - Logical Operators with Optional DataFrames 264 | 265 | func test_DataFrameLogicalAND_WithOptionalDataFrames() { 266 | let s1 = DataSeries([true, false, true]) 267 | let s2 = DataSeries([false, true, false]) 268 | let s3 = DataSeries([true, true, false]) 269 | let s4 = DataSeries([false, false, true]) 270 | 271 | let df1: DataFrame? = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 272 | let df2: DataFrame? = DataFrame(dictionaryLiteral: ("col1", s3), ("col2", s4)) 273 | 274 | let result = df1 && df2 275 | 276 | XCTAssertNotNil(result) 277 | XCTAssertEqual(result?["col1"]?[0], true) // true && true 278 | XCTAssertEqual(result?["col1"]?[1], false) // false && true 279 | XCTAssertEqual(result?["col1"]?[2], false) // true && false 280 | XCTAssertEqual(result?["col2"]?[0], false) // false && false 281 | XCTAssertEqual(result?["col2"]?[1], false) // true && false 282 | XCTAssertEqual(result?["col2"]?[2], false) // false && true 283 | } 284 | 285 | func test_DataFrameLogicalOR_WithOptionalDataFrames() { 286 | let s1 = DataSeries([true, false, true]) 287 | let s2 = DataSeries([false, true, false]) 288 | let s3 = DataSeries([true, true, false]) 289 | let s4 = DataSeries([false, false, true]) 290 | 291 | let df1: DataFrame? = DataFrame(dictionaryLiteral: ("col1", s1), ("col2", s2)) 292 | let df2: DataFrame? = DataFrame(dictionaryLiteral: ("col1", s3), ("col2", s4)) 293 | 294 | let result = df1 || df2 295 | 296 | XCTAssertNotNil(result) 297 | XCTAssertEqual(result?["col1"]?[0], true) // true || true 298 | XCTAssertEqual(result?["col1"]?[1], true) // false || true 299 | XCTAssertEqual(result?["col1"]?[2], true) // true || false 300 | XCTAssertEqual(result?["col2"]?[0], false) // false || false 301 | XCTAssertEqual(result?["col2"]?[1], true) // true || false 302 | XCTAssertEqual(result?["col2"]?[2], true) // false || true 303 | } 304 | 305 | // MARK: - DataSeries Arithmetic Operators with Optional DataSeries 306 | 307 | func test_DataSeriesAddition_WithOptionalDataSeries() { 308 | let s1: DataSeries? = DataSeries([1, 2, 3]) 309 | let s2: DataSeries? = DataSeries([4, 5, 6]) 310 | 311 | let result = s1 + s2 312 | 313 | XCTAssertNotNil(result) 314 | XCTAssertEqual(result?[0], 5) // 1 + 4 315 | XCTAssertEqual(result?[1], 7) // 2 + 5 316 | XCTAssertEqual(result?[2], 9) // 3 + 6 317 | } 318 | 319 | func test_DataSeriesAddition_WithNilDataSeries() { 320 | let s1: DataSeries? = DataSeries([1, 2, 3]) 321 | let s2: DataSeries? = nil 322 | 323 | let result = s1 + s2 324 | 325 | XCTAssertNil(result) 326 | } 327 | 328 | func test_DataSeriesSubtraction_WithOptionalDataSeries() { 329 | let s1: DataSeries? = DataSeries([10, 20, 30]) 330 | let s2: DataSeries? = DataSeries([1, 2, 3]) 331 | 332 | let result = s1 - s2 333 | 334 | XCTAssertNotNil(result) 335 | XCTAssertEqual(result?[0], 9) // 10 - 1 336 | XCTAssertEqual(result?[1], 18) // 20 - 2 337 | XCTAssertEqual(result?[2], 27) // 30 - 3 338 | } 339 | 340 | func test_DataSeriesMultiplication_WithOptionalDataSeries() { 341 | let s1: DataSeries? = DataSeries([2, 3, 4]) 342 | let s2: DataSeries? = DataSeries([5, 6, 7]) 343 | 344 | let result = s1 * s2 345 | 346 | XCTAssertNotNil(result) 347 | XCTAssertEqual(result?[0], 10) // 2 * 5 348 | XCTAssertEqual(result?[1], 18) // 3 * 6 349 | XCTAssertEqual(result?[2], 28) // 4 * 7 350 | } 351 | 352 | func test_DataSeriesDivision_WithOptionalDataSeries() { 353 | let s1: DataSeries? = DataSeries([10.0, 20.0, 30.0]) 354 | let s2: DataSeries? = DataSeries([2.0, 4.0, 5.0]) 355 | 356 | let result = s1 / s2 357 | 358 | XCTAssertNotNil(result) 359 | XCTAssertEqual(result?[0], 5.0) // 10 / 2 360 | XCTAssertEqual(result?[1], 5.0) // 20 / 4 361 | XCTAssertEqual(result?[2], 6.0) // 30 / 5 362 | } 363 | 364 | // MARK: - DataSeries Comparison with Scalars 365 | 366 | func test_DataSeriesAddition_WithScalar() { 367 | let s1: DataSeries? = DataSeries([1, 2, 3]) 368 | let scalar: Int? = 5 369 | 370 | let result = s1 + scalar 371 | 372 | XCTAssertNotNil(result) 373 | XCTAssertEqual(result?[0], 6) // 1 + 5 374 | XCTAssertEqual(result?[1], 7) // 2 + 5 375 | XCTAssertEqual(result?[2], 8) // 3 + 5 376 | } 377 | 378 | func test_DataSeriesSubtraction_WithScalar() { 379 | let s1: DataSeries? = DataSeries([10, 20, 30]) 380 | let scalar: Int? = 5 381 | 382 | let result = s1 - scalar 383 | 384 | XCTAssertNotNil(result) 385 | XCTAssertEqual(result?[0], 5) // 10 - 5 386 | XCTAssertEqual(result?[1], 15) // 20 - 5 387 | XCTAssertEqual(result?[2], 25) // 30 - 5 388 | } 389 | 390 | func test_DataSeriesMultiplication_WithScalar() { 391 | let s1: DataSeries? = DataSeries([2, 3, 4]) 392 | let scalar: Int? = 5 393 | 394 | let result = s1 * scalar 395 | 396 | XCTAssertNotNil(result) 397 | XCTAssertEqual(result?[0], 10) // 2 * 5 398 | XCTAssertEqual(result?[1], 15) // 3 * 5 399 | XCTAssertEqual(result?[2], 20) // 4 * 5 400 | } 401 | 402 | func test_DataSeriesDivision_WithScalar() { 403 | let s1: DataSeries? = DataSeries([10.0, 20.0, 30.0]) 404 | let scalar: Double? = 2.0 405 | 406 | let result = s1 / scalar 407 | 408 | XCTAssertNotNil(result) 409 | XCTAssertEqual(result?[0], 5.0) // 10 / 2 410 | XCTAssertEqual(result?[1], 10.0) // 20 / 2 411 | XCTAssertEqual(result?[2], 15.0) // 30 / 2 412 | } 413 | 414 | // MARK: - Edge Cases 415 | 416 | func test_DataFrameArithmetic_WithEmptyDataFrames() { 417 | let df1: DataFrame = [:] 418 | let df2: DataFrame = [:] 419 | 420 | let result = df1 + df2 421 | 422 | XCTAssertEqual(result.count, 0) 423 | } 424 | 425 | func test_DataSeriesArithmetic_WithEmptySeries() { 426 | let s1: DataSeries = DataSeries() 427 | let s2: DataSeries = DataSeries() 428 | 429 | let result = s1 + s2 430 | 431 | XCTAssertEqual(result.count, 0) 432 | } 433 | 434 | func test_DataFrameArithmetic_WithDifferentKeys() { 435 | let s1 = DataSeries([1, 2, 3]) 436 | let s2 = DataSeries([4, 5, 6]) 437 | 438 | let df1 = DataFrame(dictionaryLiteral: ("col1", s1)) 439 | let df2 = DataFrame(dictionaryLiteral: ("col2", s2)) 440 | 441 | // This should assert in debug mode, but we can't test assertions easily 442 | // This test documents the expected behavior 443 | XCTAssertEqual(df1.count, 1) 444 | XCTAssertEqual(df2.count, 1) 445 | } 446 | 447 | func test_DataSeriesArithmetic_WithDifferentLengths() { 448 | let s1 = DataSeries([1, 2, 3]) 449 | let s2 = DataSeries([4, 5]) 450 | 451 | // This should assert in debug mode, but we can't test assertions easily 452 | // This test documents the expected behavior 453 | XCTAssertEqual(s1.count, 3) 454 | XCTAssertEqual(s2.count, 2) 455 | } 456 | } -------------------------------------------------------------------------------- /Tests/KoalasTests/DataSeriesTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Sergey Kazakov on 04.06.2020. 6 | // 7 | 8 | import XCTest 9 | @testable import Koalas 10 | 11 | final class DataSeriesTests: XCTestCase { 12 | func test_whenShiftedPositiveByN_NilsInTheBeginningCountsNAndArraysMatch() { 13 | let n = 3 14 | let s1 = DataSeries([1, 2, 3 ,4 ,5 ,6, 7, 8]) 15 | let s2 = s1.shiftedBy(n) 16 | 17 | XCTAssertEqual(s1.count, s2.count) 18 | let allNils = s2[0.. 0 { 78 | XCTAssertEqual($0.element, (expandingSum[idx - 1] ?? 0) + (s1[idx] ?? 0)) 79 | } 80 | } 81 | } 82 | 83 | func test_expandingMax() { 84 | let s1 = DataSeries([nil, nil, 2, 3 ,1 ,0 ,1, 4, 3]) 85 | let result = s1.expandingMax() 86 | 87 | XCTAssertEqual(s1.count, result.count) 88 | 89 | let expectedResult = DataSeries([nil, nil, 2, 3 ,3 ,3 ,3, 4, 4]) 90 | XCTAssertTrue(result.equalsTo(series: expectedResult)) 91 | } 92 | 93 | func test_expandingMin() { 94 | let s1 = DataSeries([nil, nil, 2, 3 ,1 ,0 ,1, 4, 3]) 95 | let result = s1.expandingMin() 96 | 97 | XCTAssertEqual(s1.count, result.count) 98 | 99 | let expectedResult = DataSeries([nil, nil, 2, 2 ,1 ,0 ,0, 0, 0]) 100 | XCTAssertTrue(result.equalsTo(series: expectedResult)) 101 | } 102 | 103 | func test_expandingSumWithNilAndInitialNonZero() { 104 | let s1 = DataSeries([nil, 1, nil ,nil ,nil ,nil, 1, 1]) 105 | let expandingSum = s1.expandingSum(initial: 1) 106 | 107 | XCTAssertEqual(s1.count, expandingSum.count) 108 | 109 | expandingSum.enumerated().forEach { 110 | let idx = $0.offset 111 | if idx > 0 { 112 | XCTAssertEqual($0.element, (expandingSum[idx - 1] ?? 0) + (s1[idx] ?? 0)) 113 | } 114 | } 115 | } 116 | 117 | func test_scanSeries() { 118 | let s1 = DataSeries([1, 2, 3 ,4 ,5 ,6, 7, 8]) 119 | let expandingSum = s1.scanSeries(initial: 1) { ($0 ?? 0) + ($1 ?? 0) } 120 | 121 | XCTAssertEqual(s1.count, expandingSum.count) 122 | 123 | expandingSum.enumerated().forEach { 124 | let idx = $0.offset 125 | if idx > 0 { 126 | XCTAssertEqual($0.element, (expandingSum[idx - 1] ?? 0) + (s1[idx] ?? 0)) 127 | } 128 | } 129 | } 130 | 131 | func test_scanSeriesWithNilAndInitialNonZero() { 132 | let s1 = DataSeries([nil, 1, nil ,nil ,nil ,nil, 1, 1]) 133 | let expandingSum = s1.scanSeries(initial: 1) { ($0 ?? 0) + ($1 ?? 0) } 134 | XCTAssertEqual(s1.count, expandingSum.count) 135 | 136 | expandingSum.enumerated().forEach { 137 | let idx = $0.offset 138 | if idx > 0 { 139 | XCTAssertEqual($0.element, (expandingSum[idx - 1] ?? 0) + (s1[idx] ?? 0)) 140 | } 141 | } 142 | } 143 | 144 | func test_whenWindowIsUnderSeriesLength_rollingSumEqualsShiftedSubstractedCumsSums() { 145 | let first: Int = 1 146 | let last: Int = 20 147 | let window = 4 148 | 149 | let arr = Array(first...last) 150 | let s1 = DataSeries(arr) 151 | let s2 = s1.shiftedBy(window) 152 | 153 | 154 | let expandingSum1 = s1.expandingSum(initial: 0) 155 | let expandingSum2 = s2.expandingSum(initial: 0) 156 | 157 | var rollingSum1 = expandingSum1 - expandingSum2 158 | 159 | /**when index is less then window size, then nil value in rolling sum 160 | */ 161 | rollingSum1.replaceSubrange(0.. Int? in 164 | /**when array of values is less then window size, then nil value in rolling sum 165 | */ 166 | guard w.allSatisfy({ $0 != nil }) else { 167 | return nil 168 | } 169 | 170 | return w.reduce(0) { $0 + ($1 ?? 0) } 171 | } 172 | 173 | 174 | XCTAssertEqual(rollingSum1.count, rollingSum2.count) 175 | 176 | zip(rollingSum1, rollingSum2).forEach { 177 | XCTAssertEqual($0.0, $0.1) 178 | } 179 | } 180 | 181 | func test_whenWindowLargerThenSeriesLength_rollingSumInNil() { 182 | let first: Int = 1 183 | let last: Int = 20 184 | 185 | let arr = Array(first...last) 186 | let s1 = DataSeries(arr) 187 | 188 | let window = arr.count + 1 189 | let expandingSum1 = s1.expandingSum(initial: 0) 190 | let expandingSum2 = expandingSum1.shiftedBy(window) 191 | 192 | let rollingSum1 = expandingSum1 - expandingSum2 193 | 194 | let rollingSum2 = s1.rollingFunc(initial: nil, window: window) { (w: [Int?]) -> Int? in 195 | guard w.allSatisfy({ $0 != nil }) else { 196 | return nil 197 | } 198 | 199 | return w.reduce(0) { $0 + ($1 ?? 0) } 200 | } 201 | 202 | XCTAssertEqual(rollingSum1.count, rollingSum2.count) 203 | XCTAssertTrue(rollingSum2.allSatisfy { $0 == nil }) 204 | zip(rollingSum1, rollingSum2).forEach { 205 | XCTAssertEqual($0.0, $0.1) 206 | } 207 | 208 | } 209 | 210 | func test_whenWindowEqualsSeriesLength_rollingSumLastValueNotNil() { 211 | let first: Int = 1 212 | let last: Int = 20 213 | 214 | let arr = Array(first...last) 215 | let s1 = DataSeries(arr) 216 | 217 | let window = arr.count 218 | let expandingSum1 = s1.expandingSum(initial: 0) 219 | var expandingSum2 = expandingSum1.shiftedBy(window) 220 | expandingSum2[window - 1] = 0 //otherwise rolling sum at this point would be wrong due to nil 221 | let rollingSum1 = expandingSum1 - expandingSum2 222 | 223 | let rollingSum2 = s1.rollingFunc(initial: nil, window: window) { (w: [Int?]) -> Int? in 224 | guard w.allSatisfy({ $0 != nil }) else { 225 | return nil 226 | } 227 | 228 | return w.reduce(0) { $0 + ($1 ?? 0) } 229 | } 230 | XCTAssertEqual(rollingSum1.count, rollingSum2.count) 231 | 232 | XCTAssertEqual(rollingSum1.enumerated().first { $0.element != nil}?.offset, window - 1) 233 | zip(rollingSum1, rollingSum2).forEach { 234 | XCTAssertEqual($0.0, $0.1) 235 | } 236 | } 237 | 238 | func test_whenMapToContant_equalLengthAndValueMatch() { 239 | let first: Int = 1 240 | let last: Int = 20 241 | 242 | let constant = 1 243 | let arr = Array(first...last) 244 | 245 | let s1 = DataSeries(arr) 246 | let s2 = s1.mapTo(constant: constant) 247 | 248 | XCTAssertEqual(s2.count, s1.count) 249 | s2.forEach { XCTAssertEqual($0, constant) } 250 | } 251 | 252 | func test_whenSeriesLengthEqual_MemberwiseSum() { 253 | let first: Int = 1 254 | let last: Int = 20 255 | 256 | let arr = Array(first...last) 257 | 258 | let s1 = DataSeries(arr) 259 | let s2 = DataSeries(arr) 260 | 261 | let s3 = s1 + s2 262 | 263 | XCTAssertEqual(s3.count, s1.count) 264 | s3.enumerated().forEach { XCTAssertEqual($0.element!, s2[$0.offset]! + s1[$0.offset]!) } 265 | } 266 | 267 | func test_whenSeriesLengthEqual_MemberwiseDiff() { 268 | let first: Int = 1 269 | let last: Int = 20 270 | 271 | let arr = Array(first...last) 272 | 273 | let s1 = DataSeries(arr) 274 | let s2 = DataSeries(arr) 275 | 276 | let s3 = s1 - s2 277 | 278 | XCTAssertEqual(s3.count, s1.count) 279 | s3.enumerated().forEach { XCTAssertEqual($0.element!, s2[$0.offset]! - s1[$0.offset]!) } 280 | } 281 | 282 | func test_whenSeriesLengthEqual_MemberwiseProd() { 283 | let first: Int = 1 284 | let last: Int = 20 285 | 286 | let arr = Array(first...last) 287 | 288 | let s1 = DataSeries(arr) 289 | let s2 = DataSeries(arr) 290 | 291 | let s3 = s1 * s2 292 | 293 | XCTAssertEqual(s3.count, s1.count) 294 | s3.enumerated().forEach { XCTAssertEqual($0.element!, s2[$0.offset]! * s1[$0.offset]!) } 295 | } 296 | 297 | func test_whenSeriesLengthEqual_MemberwiseDevide() { 298 | let first: Double = 1.0 299 | let last: Double = 20.0 300 | let arr = Array(stride(from: first, through: last, by: 1.0)) 301 | 302 | let s1 = DataSeries(arr) 303 | let s2 = DataSeries(arr) 304 | 305 | let s3 = s1 / s2 306 | 307 | XCTAssertEqual(s3.count, s1.count) 308 | s3.enumerated().forEach { XCTAssertEqual($0.element!, s2[$0.offset]! / s1[$0.offset]!) } 309 | } 310 | 311 | func test_whenSeriesLengthEqual_MemberwiseConditionalMap() { 312 | 313 | let s1 = DataSeries(repeating: 1, count: 5) 314 | let s2 = DataSeries(repeating: 2, count: 5) 315 | let s3 = DataSeries([true, false, true, nil, false]) 316 | 317 | guard let result = s3.whereTrue(then: s1, else: s2) else { 318 | XCTFail("Not equal length") 319 | return 320 | } 321 | 322 | result.enumerated().forEach { 323 | if let s3Value = s3[$0.offset] { 324 | 325 | if s3Value { 326 | XCTAssertEqual($0.element, s1[$0.offset]) 327 | } else { 328 | XCTAssertEqual($0.element, s2[$0.offset]) 329 | } 330 | } else { 331 | XCTAssertEqual($0.element, nil) 332 | } 333 | 334 | } 335 | } 336 | 337 | func test_whenSeriesContainsNoNils_sum() { 338 | let first: Int = 1 339 | let last: Int = 20 340 | 341 | let arr = Array(first...last) 342 | 343 | let s1 = DataSeries(arr) 344 | XCTAssertEqual(s1.sum(ignoreNils: true), arr.reduce(0, +)) 345 | } 346 | 347 | func test_whenSeriesContainsDoubleAndNoNils_sum() { 348 | let first: Double = 1 349 | let last: Double = 20 350 | 351 | let arr = Array(stride(from: first, through: last, by: 1.0)) 352 | 353 | let s1 = DataSeries(arr) 354 | XCTAssertEqual(s1.sum(ignoreNils: true), arr.reduce(0, +)) 355 | } 356 | 357 | func test_whenSeriesContainsNills_sumWithIgnoreNils() { 358 | let first: Int = 1 359 | let last: Int = 20 360 | 361 | let arr = Array(first...last) 362 | 363 | var s1 = DataSeries(arr) 364 | s1.append(nil) 365 | XCTAssertEqual(s1.sum(ignoreNils: true), arr.reduce(0, +)) 366 | } 367 | 368 | func test_whenSeriesContainsNills_sumWithNoIgnoreNilsEqualsNil() { 369 | let first: Int = 1 370 | let last: Int = 20 371 | 372 | let arr = Array(first...last) 373 | 374 | var s1 = DataSeries(arr) 375 | s1.append(nil) 376 | 377 | XCTAssertNil(s1.sum(ignoreNils: false)) 378 | } 379 | 380 | func test_whenSeriesContainsNoNils_mean() { 381 | let first: Double = 1 382 | let last: Double = 20 383 | 384 | let arr = Array(stride(from: first, through: last, by: 1.0)) 385 | 386 | let s1 = DataSeries(arr) 387 | XCTAssertEqual(s1.mean(shouldSkipNils: true), arr.reduce(0, +) / Double(arr.count)) 388 | } 389 | 390 | func test_whenSeriesContainsNils_meanWithIgnoreNils() { 391 | let first: Double = 1 392 | let last: Double = 20 393 | 394 | let arr = Array(stride(from: first, through: last, by: 1.0)) 395 | 396 | var s1 = DataSeries(arr) 397 | s1.append(nil) 398 | 399 | XCTAssertEqual(s1.mean(shouldSkipNils: true), arr.reduce(0, +) / Double(arr.count)) 400 | } 401 | 402 | func test_whenSeriesContainsNils_meanNoIgnoreNils() { 403 | let first: Double = 1 404 | let last: Double = 20 405 | 406 | var arr = Array(stride(from: first, through: last, by: 1.0)) 407 | 408 | var s1 = DataSeries(arr) 409 | s1.append(contentsOf: [nil, nil, nil]) 410 | arr.append(contentsOf: [0, 0, 0]) 411 | 412 | XCTAssertEqual(s1.mean(shouldSkipNils: false), arr.reduce(0, +) / Double(arr.count)) 413 | } 414 | 415 | func test_forwardFillNils() { 416 | let arr1 = [1, 2, nil, 3, nil, 4, nil, 5, nil, nil, nil] 417 | let expectedArr = [1, 2, 2, 3, 3, 4, 4, 5, 5, 5, 5] 418 | let s1 = DataSeries(arr1) 419 | let s2 = s1.fillNils(method: .forward(initial: 0)) 420 | 421 | XCTAssertEqual(s1.count, s2.count) 422 | zip(s2, expectedArr).forEach { XCTAssertEqual($0.0, $0.1) } 423 | } 424 | 425 | func test_whenFirstNil_forwardFillNilsStartWithInitial() { 426 | let initial = 0 427 | let arr1 = [nil, 2, nil, 3, nil, 4, nil, 5, nil, nil, nil] 428 | let expectedArr = [initial, 2, 2, 3, 3, 4, 4, 5, 5, 5, 5] 429 | let s1 = DataSeries(arr1) 430 | let s2 = s1.fillNils(method: .forward(initial: initial)) 431 | 432 | XCTAssertEqual(s1.count, s2.count) 433 | zip(s2, expectedArr).forEach { XCTAssertEqual($0.0, $0.1) } 434 | } 435 | 436 | func test_backwardFillNils() { 437 | let arr1 = [1, 2, nil, 3, nil, 4, nil, 5, nil, nil, 6] 438 | let expectedArr = [1, 2, 3, 3, 4, 4, 5, 5, 6, 6, 6] 439 | let s1 = DataSeries(arr1) 440 | let s2 = s1.fillNils(method: .backward(initial: 0)) 441 | 442 | XCTAssertEqual(s1.count, s2.count) 443 | zip(s2, expectedArr).forEach { XCTAssertEqual($0.0, $0.1) } 444 | } 445 | 446 | func test_whenLastNil_backwardFillNils() { 447 | let initial = 6 448 | let arr1 = [1, 2, nil, 3, nil, 4, nil, 5, nil, nil, nil] 449 | let expectedArr = [1, 2, 3, 3, 4, 4, 5, 5, initial, initial, initial] 450 | let s1 = DataSeries(arr1) 451 | let s2 = s1.fillNils(method: .backward(initial: initial)) 452 | 453 | XCTAssertEqual(s1.count, s2.count) 454 | zip(s2, expectedArr).forEach { XCTAssertEqual($0.0, $0.1) } 455 | } 456 | 457 | func test_toDateComponentsFunc() { 458 | let dateFormatter = DateFormatter() 459 | dateFormatter.dateFormat = "yyyy/MM/dd" 460 | 461 | let years = [2011, 2019, 2020] 462 | let months = [1, 2, 3] 463 | let days = [3, 4, 5] 464 | 465 | let s1 = DataSeries([ 466 | dateFormatter.date(from: "\(years[0])/\(months[0])/\(days[0])"), 467 | dateFormatter.date(from: "\(years[1])/\(months[1])/\(days[1])"), 468 | dateFormatter.date(from: "\(years[2])/\(months[2])/\(days[2])") 469 | ]) 470 | 471 | let dateComponentsDF = s1.toDateComponents() 472 | dateComponentsDF[.year]?.enumerated() 473 | .forEach { XCTAssertEqual($0.element, years[$0.offset]) } 474 | 475 | dateComponentsDF[.month]?.enumerated() 476 | .forEach { XCTAssertEqual($0.element, months[$0.offset]) } 477 | 478 | dateComponentsDF[.day]?.enumerated() 479 | .forEach { XCTAssertEqual($0.element, days[$0.offset]) } 480 | } 481 | 482 | func test_whenSeriesEqual_equalToReturnsTrue() { 483 | 484 | let first: Int = 1 485 | let last: Int = 20 486 | 487 | let arr = Array(first...last) 488 | 489 | let s1 = DataSeries(arr) 490 | let s2 = DataSeries(arr) 491 | 492 | XCTAssertTrue(s1.equalsTo(series: s2)) 493 | } 494 | 495 | func test_whenSeriesEqualAndContainNil_equalToReturnsTrue() { 496 | 497 | let first: Int = 1 498 | let last: Int = 20 499 | 500 | let arr = Array(first...last) 501 | 502 | let s1 = DataSeries([nil, nil, 1, 2, 3]) 503 | let s2 = DataSeries([nil, nil, 1, 2, 3]) 504 | 505 | XCTAssertTrue(s1.equalsTo(series: s2)) 506 | } 507 | 508 | func test_whenFloatingPointSeriesEqual_equalToReturnsTrue() { 509 | 510 | let first: Double = 1 511 | let last: Double = 20 512 | 513 | let arr = Array(stride(from: first, through: last, by: 1.0)) 514 | let arr2 = Array(stride(from: first, through: last, by: 1.0)) 515 | 516 | 517 | let s1 = DataSeries(arr) 518 | let s2 = DataSeries(arr2) 519 | 520 | XCTAssertTrue(s1.equalsTo(series: s2, with: 0.000001)) 521 | } 522 | 523 | func test_whenSeriesNotEqual_equalsToReturnsFalse() { 524 | 525 | let first: Int = 1 526 | let last: Int = 20 527 | 528 | let arr = Array(first...last) 529 | 530 | let s1 = DataSeries(arr) 531 | let s2 = DataSeries(arr.reversed()) 532 | 533 | XCTAssertFalse(s1.equalsTo(series: s2)) 534 | } 535 | 536 | func test_whenSeriesNotEqualLength_equalsToReturnsFalse() { 537 | 538 | let first: Int = 1 539 | let last: Int = 20 540 | 541 | let arr = Array(first...last) 542 | let arr2 = Array(first..? = DataSeries([1, 2, 3]) 556 | let constToCompare = 2 557 | 558 | let expectedResult1 = DataSeries([true, false, false]) 559 | let expectedResult2 = DataSeries([false, false, true]) 560 | 561 | XCTAssertTrue(expectedResult1.equalsTo(series: s1 < constToCompare)) 562 | XCTAssertTrue(expectedResult2.equalsTo(series: s1 > constToCompare)) 563 | } 564 | 565 | 566 | func test_nonStrictCompareToConst() { 567 | 568 | let s1: DataSeries? = DataSeries([1, 2, 3]) 569 | let constToCompare = 2 570 | 571 | let expectedResult1 = DataSeries([true, true, false]) 572 | let expectedResult2 = DataSeries([false, true, true]) 573 | 574 | XCTAssertTrue(expectedResult1.equalsTo(series: s1 <= constToCompare)) 575 | XCTAssertTrue(expectedResult2.equalsTo(series: s1 >= constToCompare)) 576 | } 577 | 578 | func test_equalityCompareToConst() { 579 | let s1: DataSeries? = DataSeries([1, 2, 3]) 580 | let constToCompare = 2 581 | 582 | let expectedResult = DataSeries([false, true, false]) 583 | 584 | XCTAssertTrue(expectedResult.equalsTo(series: s1 == constToCompare)) 585 | } 586 | 587 | func test_nonEqualityCompareToConst() { 588 | 589 | let s1: DataSeries? = DataSeries([1, 2, 3]) 590 | let constToCompare = 2 591 | 592 | let expectedResult = DataSeries([true, false, true]) 593 | 594 | XCTAssertTrue(expectedResult.equalsTo(series: s1 != constToCompare)) 595 | } 596 | 597 | func test_strictCompare() { 598 | 599 | let s1: DataSeries? = DataSeries([1, 2, 3]) 600 | let s2: DataSeries? = DataSeries([2, 2, 2]) 601 | 602 | let expectedResult = DataSeries([true, false, false]) 603 | 604 | XCTAssertTrue(expectedResult.equalsTo(series: s1 < s2)) 605 | XCTAssertTrue(expectedResult.equalsTo(series: s2 > s1)) 606 | } 607 | 608 | func test_nonStrictCompare() { 609 | 610 | let s1: DataSeries? = DataSeries([1, 2, 3]) 611 | let s2: DataSeries? = DataSeries([2, 2, 2]) 612 | 613 | let expectedResult = DataSeries([true, true, false]) 614 | 615 | XCTAssertTrue(expectedResult.equalsTo(series: s1 <= s2)) 616 | XCTAssertTrue(expectedResult.equalsTo(series: s2 >= s1)) 617 | } 618 | 619 | func test_equalityCompare() { 620 | 621 | let s1: DataSeries? = DataSeries([1, 2, 3]) 622 | let s2: DataSeries? = DataSeries([2, 2, 2]) 623 | 624 | let expectedResult = DataSeries([false, true, false]) 625 | 626 | XCTAssertTrue(expectedResult.equalsTo(series: s1 == s2)) 627 | } 628 | 629 | func test_nonEqualityCompare() { 630 | 631 | let s1: DataSeries? = DataSeries([1, 2, 3]) 632 | let s2: DataSeries? = DataSeries([2, 2, 2]) 633 | 634 | let expectedResult = DataSeries([true, false, true]) 635 | 636 | XCTAssertTrue(expectedResult.equalsTo(series: s1 != s2)) 637 | } 638 | 639 | func test_isEmptySeries() { 640 | let s1: DataSeries = DataSeries([nil, nil, nil]) 641 | let s2: DataSeries = DataSeries([1, 2, nil]) 642 | let s3: DataSeries = DataSeries([1, 2, 3]) 643 | 644 | XCTAssertTrue(s1.isEmptySeries()) 645 | XCTAssertFalse(s2.isEmptySeries()) 646 | XCTAssertFalse(s3.isEmptySeries()) 647 | } 648 | } 649 | --------------------------------------------------------------------------------