diff --git a/ReactiveKit.podspec b/ReactiveKit.podspec index ebe2728..2631e2b 100644 --- a/ReactiveKit.podspec +++ b/ReactiveKit.podspec @@ -1,12 +1,12 @@ Pod::Spec.new do |s| s.name = "ReactiveKit" - s.version = "2.0.3" + s.version = "2.0.4" s.summary = "A Swift Reactive Programming Framework" s.description = "ReactiveKit is a collection of Swift frameworks for reactive and functional reactive programming." s.homepage = "https://github.com/ReactiveKit/ReactiveKit" s.license = 'MIT' s.author = { "Srdan Rasic" => "srdan.rasic@gmail.com" } - s.source = { :git => "https://github.com/ReactiveKit/ReactiveKit.git", :tag => "v2.0.3" } + s.source = { :git => "https://github.com/ReactiveKit/ReactiveKit.git", :tag => "v2.0.4" } s.ios.deployment_target = '8.0' s.osx.deployment_target = '10.9' diff --git a/ReactiveKit.xcodeproj/project.pbxproj b/ReactiveKit.xcodeproj/project.pbxproj index 310b11c..a5a4080 100644 --- a/ReactiveKit.xcodeproj/project.pbxproj +++ b/ReactiveKit.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 168103451D046388007D1DD4 /* CollectionPropertyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 168103431D046388007D1DD4 /* CollectionPropertyTests.swift */; }; + 168103461D046388007D1DD4 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 168103441D046388007D1DD4 /* Helpers.swift */; }; 16A6B8F11CE7952600C27D5B /* RKProtocolProxyBase.h in Headers */ = {isa = PBXBuildFile; fileRef = 16A6B8EF1CE7952600C27D5B /* RKProtocolProxyBase.h */; settings = {ATTRIBUTES = (Public, ); }; }; 16A6B8F21CE7952600C27D5B /* RKProtocolProxyBase.h in Headers */ = {isa = PBXBuildFile; fileRef = 16A6B8EF1CE7952600C27D5B /* RKProtocolProxyBase.h */; settings = {ATTRIBUTES = (Public, ); }; }; 16A6B8F31CE7952600C27D5B /* RKProtocolProxyBase.h in Headers */ = {isa = PBXBuildFile; fileRef = 16A6B8EF1CE7952600C27D5B /* RKProtocolProxyBase.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -23,7 +25,6 @@ 16C33B841BEFBAC900A0DBE0 /* ReactiveKit.h in Headers */ = {isa = PBXBuildFile; fileRef = ECBCCDD31BEB6B9A00723476 /* ReactiveKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; 16C33B851BEFBAC900A0DBE0 /* ReactiveKit.h in Headers */ = {isa = PBXBuildFile; fileRef = ECBCCDD31BEB6B9A00723476 /* ReactiveKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; EC2515771CBD4E0700175926 /* StreamTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC2515751CBD4E0000175926 /* StreamTests.swift */; }; - EC25158D1CBF94B400175926 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC25158C1CBF94B400175926 /* Helpers.swift */; }; EC25158F1CBFB6A400175926 /* ArrayDiffTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC8A9A171CABDA5F0042A6AD /* ArrayDiffTests.swift */; }; EC65A0B31CBFEE4D00B41FA7 /* OperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC65A0B21CBFEE4D00B41FA7 /* OperationTests.swift */; }; EC8A99E71CABD9B50042A6AD /* CollectionProperty.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC8A99DB1CABD9B50042A6AD /* CollectionProperty.swift */; }; @@ -96,6 +97,8 @@ 162CB7461CB451D200FB6375 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; 1671707E1BF4F62A001786CE /* ReactiveKit.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = ReactiveKit.podspec; sourceTree = SOURCE_ROOT; }; 1671707F1BF4F64E001786CE /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; lineEnding = 0; path = README.md; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.markdown; }; + 168103431D046388007D1DD4 /* CollectionPropertyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CollectionPropertyTests.swift; path = Tests/CollectionPropertyTests.swift; sourceTree = SOURCE_ROOT; }; + 168103441D046388007D1DD4 /* Helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Helpers.swift; path = Tests/Helpers.swift; sourceTree = SOURCE_ROOT; }; 16A6B8EF1CE7952600C27D5B /* RKProtocolProxyBase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKProtocolProxyBase.h; sourceTree = ""; }; 16A6B8F01CE7952600C27D5B /* RKProtocolProxyBase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKProtocolProxyBase.m; sourceTree = ""; }; 16A6B8F91CE7957400C27D5B /* ProtocolProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ProtocolProxy.swift; path = Sources/ProtocolProxy.swift; sourceTree = SOURCE_ROOT; }; @@ -103,7 +106,6 @@ 16C33B161BEFB9CB00A0DBE0 /* ReactiveKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ReactiveKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 16C33B241BEFBA0100A0DBE0 /* ReactiveKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ReactiveKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; EC2515751CBD4E0000175926 /* StreamTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = StreamTests.swift; path = Tests/StreamTests.swift; sourceTree = SOURCE_ROOT; }; - EC25158C1CBF94B400175926 /* Helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = ""; }; EC65A0B21CBFEE4D00B41FA7 /* OperationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OperationTests.swift; path = Tests/OperationTests.swift; sourceTree = SOURCE_ROOT; }; EC8A99DB1CABD9B50042A6AD /* CollectionProperty.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CollectionProperty.swift; path = Sources/CollectionProperty.swift; sourceTree = SOURCE_ROOT; }; EC8A99DC1CABD9B50042A6AD /* Disposable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Disposable.swift; path = Sources/Disposable.swift; sourceTree = SOURCE_ROOT; }; @@ -223,7 +225,8 @@ EC8A9A181CABDA5F0042A6AD /* PerformanceTests.swift */, EC2515751CBD4E0000175926 /* StreamTests.swift */, EC65A0B21CBFEE4D00B41FA7 /* OperationTests.swift */, - EC25158C1CBF94B400175926 /* Helpers.swift */, + 168103431D046388007D1DD4 /* CollectionPropertyTests.swift */, + 168103441D046388007D1DD4 /* Helpers.swift */, ECBCCDE11BEB6B9B00723476 /* Info.plist */, ); path = ReactiveKitTests; @@ -540,9 +543,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - EC25158D1CBF94B400175926 /* Helpers.swift in Sources */, + 168103451D046388007D1DD4 /* CollectionPropertyTests.swift in Sources */, EC2515771CBD4E0700175926 /* StreamTests.swift in Sources */, EC25158F1CBFB6A400175926 /* ArrayDiffTests.swift in Sources */, + 168103461D046388007D1DD4 /* Helpers.swift in Sources */, EC65A0B31CBFEE4D00B41FA7 /* OperationTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/ReactiveKit/Info.plist b/ReactiveKit/Info.plist index 0de69c1..241ef86 100644 --- a/ReactiveKit/Info.plist +++ b/ReactiveKit/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 2.0.3 + 2.0.4 CFBundleSignature ???? CFBundleVersion diff --git a/Sources/CollectionProperty.swift b/Sources/CollectionProperty.swift index 39a71a1..dab7f22 100644 --- a/Sources/CollectionProperty.swift +++ b/Sources/CollectionProperty.swift @@ -30,7 +30,7 @@ public protocol CollectionChangesetType { var updates: [Collection.Index] { get } } -public struct CollectionChangeset: CollectionChangesetType { +public struct CollectionChangeset: CollectionChangesetType, CustomStringConvertible { public let collection: Collection public let inserts: [Collection.Index] public let deletes: [Collection.Index] @@ -46,6 +46,10 @@ public struct CollectionChangeset: CollectionChanges self.deletes = deletes self.updates = updates } + + public var description: String { + return "Collection: \(collection), changes: i\(inserts), d\(deletes), u\(updates)." + } } public extension CollectionChangeset { @@ -74,17 +78,42 @@ public extension CollectionChangesetType where Collection.Index == Int { public extension CollectionChangesetType where Collection.Index == Int { /// O(n) - public func filter(include: Collection.Generator.Element -> Bool) -> CollectionChangeset> { + public func filter(previousIndexMap: [Int: Int], include: Collection.Generator.Element -> Bool) -> ([Int: Int], CollectionChangeset>?) { + var filtered: [Collection.Generator.Element] = [] + var indexMap: [Int: Int] = [:] + var iterator = 0 + + filtered.reserveCapacity(collection.count) + + for (index, element) in collection.enumerate() { + if include(element) { + filtered.append(element) + indexMap[index] = iterator + iterator += 1 + } + } - let filteredPairs = zip(collection.indices, collection).filter { include($0.1) } - let includedIndices = Set(filteredPairs.map { $0.0 }) + var filteredInserts = inserts.flatMap { indexMap[$0] } + var filteredDeletes = deletes.flatMap { previousIndexMap[$0] } + var filteredUpdates: [Int] = [] - let filteredCollection = filteredPairs.map { $0.1 } - let filteredInserts = inserts.filter { includedIndices.contains($0) } - let filteredDeletes = deletes.filter { includedIndices.contains($0) } - let filteredUpdates = updates.filter { includedIndices.contains($0) } + for update in updates { + if let index = indexMap[update] { + if let _ = previousIndexMap[update] { + filteredUpdates.append(index) + } else { + filteredInserts.append(index) + } + } else if let index = previousIndexMap[update] { + filteredDeletes.append(index) + } + } - return CollectionChangeset(collection: filteredCollection, inserts: filteredInserts, deletes: filteredDeletes, updates: filteredUpdates) + if inserts.count + deletes.count + updates.count > 0 && filteredInserts.count + filteredDeletes.count + filteredUpdates.count == 0 { + return (indexMap, nil) + } else { + return (indexMap, CollectionChangeset(collection: filtered, inserts: filteredInserts, deletes: filteredDeletes, updates: filteredUpdates)) + } } } @@ -104,11 +133,23 @@ public extension CollectionChangesetType where Collection.Index: Hashable { } let sortedCollection = sortedPairs.map { $0.1 } - let newDeletes = deletes.map { previousSortMap![$0]! } - let newInserts = inserts.map { sortMap[$0]! } - let newUpdates = updates.map { sortMap[$0]! } - let changeSet = CollectionChangeset(collection: sortedCollection, inserts: newInserts, deletes: newDeletes, updates: newUpdates) + var newDeletes = deletes.map { previousSortMap![$0]! } + var newInserts = inserts.map { sortMap[$0]! } + var newUpdates: [Int] = [] + + for update in updates { + if let index = sortMap[update], prevIndex = previousSortMap![update] { + if index == prevIndex { + newUpdates.append(index) + } else { + newDeletes.append(prevIndex) + newInserts.append(index) + } + } + } + + let changeSet = CollectionChangeset(collection: sortedCollection, inserts: newInserts, deletes: newDeletes, updates: newUpdates) return (changeset: changeSet, sortMap: sortMap) } } @@ -121,11 +162,23 @@ public extension CollectionChangesetType where Collection.Index: Equatable { let sortedIndices = sortedPairs.map { $0.0 } let sortedCollection = sortedPairs.map { $0.1 } - let newDeletes = deletes.map { previousSortedIndices!.indexOf($0)! } - let newInserts = inserts.map { sortedIndices.indexOf($0)! } - let newUpdates = updates.map { sortedIndices.indexOf($0)! } - let changeset = CollectionChangeset(collection: sortedCollection, inserts: newInserts, deletes: newDeletes, updates: newUpdates) + var newDeletes = deletes.map { previousSortedIndices!.indexOf($0)! } + var newInserts = inserts.map { sortedIndices.indexOf($0)! } + var newUpdates: [Array.Index] = [] + + for update in updates { + if let index = sortedIndices.indexOf(update), prevIndex = previousSortedIndices!.indexOf(update) { + if index == prevIndex { + newUpdates.append(index) + } else { + newDeletes.append(prevIndex) + newInserts.append(index) + } + } + } + + let changeset = CollectionChangeset(collection: sortedCollection, inserts: newInserts, deletes: newDeletes, updates: newUpdates) return (changeset: changeset, sortedIndices: sortedIndices) } } @@ -246,8 +299,17 @@ public extension CollectionPropertyType where Collection.Index == Int { @warn_unused_result public func filter(include: Collection.Generator.Element -> Bool) -> Stream>> { return Stream { observer in + var previousIndexMap: [Int: Int] = [:] return self.observe { event in - observer.observer(event.map { $0.filter(include) }) + if let changeset = event.element { + let (indexMap, filteredChanges) = changeset.filter(previousIndexMap, include: include) + previousIndexMap = indexMap + if let filteredChanges = filteredChanges { + observer.next(filteredChanges) + } + } else { + observer.completed() + } } } } diff --git a/Sources/Operation.swift b/Sources/Operation.swift index 6e652e4..a6d7b35 100644 --- a/Sources/Operation.swift +++ b/Sources/Operation.swift @@ -198,6 +198,20 @@ public extension OperationType { } } +extension OperationEvent: CustomStringConvertible { + + public var description: String { + switch self { + case .Next(let element): + return ".Next(\(element))" + case .Failure(let error): + return ".Failure(\(error))" + case .Completed: + return ".Completed" + } + } +} + // MARK: - Operation /// Represents a stream that can fail. diff --git a/Sources/Stream.swift b/Sources/Stream.swift index ccf18b3..f840bc7 100644 --- a/Sources/Stream.swift +++ b/Sources/Stream.swift @@ -137,6 +137,18 @@ public extension StreamType { } } +extension StreamEvent: CustomStringConvertible { + + public var description: String { + switch self { + case .Next(let element): + return ".Next(\(element))" + case .Completed: + return ".Completed" + } + } +} + // MARK: - Stream /// Represents a stream over generic Element type. diff --git a/Tests/CollectionPropertyTests.swift b/Tests/CollectionPropertyTests.swift new file mode 100644 index 0000000..ae7a9b5 --- /dev/null +++ b/Tests/CollectionPropertyTests.swift @@ -0,0 +1,225 @@ +// +// CollectionPropertyTests.swift +// ReactiveKit +// +// Created by Srdan Rasic on 05/06/16. +// Copyright © 2016 Srdan Rasic. All rights reserved. +// + +import XCTest +@testable import ReactiveKit + +class CollectionPropertyTests: XCTestCase { + + var collection: CollectionProperty<[Int]>! + + override func setUp() { + collection = CollectionProperty([1, 2, 3]) + } + + func testFiresInitial() { + collection.expectNext([CollectionChangeset(collection: [1, 2, 3], inserts: [], deletes: [], updates: [])]) + } + + func testArrayAppend() { + collection.skip(1).expectNext([CollectionChangeset(collection: [1, 2, 3, 4], inserts: [3], deletes: [], updates: [])]) + collection.append(4) + } + + func testArrayInsert() { + collection.skip(1).expectNext([CollectionChangeset(collection: [0, 1, 2, 3], inserts: [0], deletes: [], updates: [])]) + collection.insert(0, atIndex: 0) + } + + func testArrayRemoveLast() { + collection.skip(1).expectNext([CollectionChangeset(collection: [1, 2], inserts: [], deletes: [2], updates: [])]) + collection.removeLast() + } + + func testArrayRemoveAtIndex() { + collection.skip(1).expectNext([CollectionChangeset(collection: [2, 3], inserts: [], deletes: [0], updates: [])]) + collection.removeAtIndex(0) + } + + func testArrayUpdate() { + collection.skip(1).expectNext([CollectionChangeset(collection: [10, 2, 3], inserts: [], deletes: [], updates: [0])]) + collection[0] = 10 + } + + + func testArrayMapAppend() { + collection.map { $0 * 2 }.skip(1).expectNext([CollectionChangeset(collection: [2, 4, 6, 8], inserts: [3], deletes: [], updates: [])]) + collection.append(4) + } + + func testArrayMapInsert() { + collection.map { $0 * 2 }.skip(1).expectNext([CollectionChangeset(collection: [20, 2, 4, 6], inserts: [0], deletes: [], updates: [])]) + collection.insert(10, atIndex: 0) + } + + func testArrayMapRemoveLast() { + collection.map { $0 * 2 }.skip(1).expectNext([CollectionChangeset(collection: [2, 4], inserts: [], deletes: [2], updates: [])]) + collection.removeLast() + } + + func testArrayMapRemoveAtindex() { + collection.map { $0 * 2 }.skip(1).expectNext([CollectionChangeset(collection: [2, 6], inserts: [], deletes: [1], updates: [])]) + collection.removeAtIndex(1) + } + + func testArrayMapUpdate() { + collection.map { $0 * 2 }.skip(1).expectNext([CollectionChangeset(collection: [2, 40, 6], inserts: [], deletes: [], updates: [1])]) + collection[1] = 20 + } + + + func testArraySortAppendHighest() { + collection.sort(>).skip(1).expectNext([CollectionChangeset(collection: [4, 3, 2, 1], inserts: [0], deletes: [], updates: [])]) + collection.append(4) + } + + func testArraySortAppendLowest() { + collection.sort(>).skip(1).expectNext([CollectionChangeset(collection: [3, 2, 1, 0], inserts: [3], deletes: [], updates: [])]) + collection.append(0) + } + + func testArraySortInsertHighest() { + collection.sort(>).skip(1).expectNext([CollectionChangeset(collection: [4, 3, 2, 1], inserts: [0], deletes: [], updates: [])]) + collection.insert(4, atIndex: 1) + } + + func testArraySortInsertLowest() { + collection.sort(>).skip(1).expectNext([CollectionChangeset(collection: [3, 2, 1, 0], inserts: [3], deletes: [], updates: [])]) + collection.insert(0, atIndex: 1) + } + + func testArraySortRemoveHighest() { + collection.sort(>).skip(1).expectNext([CollectionChangeset(collection: [2, 1], inserts: [], deletes: [0], updates: [])]) + collection.removeLast() + } + + func testArraySortRemoveLowest() { + collection.sort(>).skip(1).expectNext([CollectionChangeset(collection: [3, 2], inserts: [], deletes: [2], updates: [])]) + collection.removeAtIndex(0) + } + + func testArraySortUpdateDoesNotChangePosition() { + collection.sort(>).skip(1).expectNext([CollectionChangeset(collection: [3, 2, 0], inserts: [], deletes: [], updates: [2])]) + collection[0] = 0 + } + + func testArraySortUpdateDoesNotChangePosition2() { + collection.sort(>).skip(1).expectNext([CollectionChangeset(collection: [4, 2, 1], inserts: [], deletes: [], updates: [0])]) + collection[2] = 4 + } + + func testArraySortUpdateMovesToFront() { + collection.sort(>).skip(1).expectNext([CollectionChangeset(collection: [4, 3, 1], inserts: [0], deletes: [1], updates: [])]) + collection[1] = 4 + } + + func testArraySortUpdateMovesToBack() { + collection.sort(>).skip(1).expectNext([CollectionChangeset(collection: [3, 1, 0], inserts: [2], deletes: [1], updates: [])]) + collection[1] = 0 + } + + + func testArrayFilterAppendNonPassing() { + collection.filter { $0 % 2 != 0 }.skip(1).expectNext([]) + collection.append(4) + } + + func testArrayFilterAppendPassing() { + collection.filter { $0 % 2 != 0 }.skip(1).expectNext([CollectionChangeset(collection: [1, 3, 5], inserts: [2], deletes: [], updates: [])]) + collection.append(5) + } + + func testArrayFilterInsertNonPassing() { + collection.filter { $0 % 2 != 0 }.skip(1).expectNext([]) + collection.insert(4, atIndex: 1) + } + + func testArrayFilterInsertPassing() { + collection.filter { $0 % 2 != 0 }.skip(1).expectNext([CollectionChangeset(collection: [1, 5, 3], inserts: [1], deletes: [], updates: [])]) + collection.insert(5, atIndex: 1) + } + + func testArrayFilterRemovePassing() { + collection.filter { $0 % 2 != 0 }.skip(1).expectNext([CollectionChangeset(collection: [1], inserts: [], deletes: [1], updates: [])]) + collection.removeLast() + } + + func testArrayFilterRemoveNonPassing() { + collection.filter { $0 % 2 != 0 }.skip(1).expectNext([]) + collection.removeAtIndex(1) + } + + func testArrayFilterUpdateNonPassingToNonPassing() { + collection.filter { $0 % 2 != 0 }.skip(1).expectNext([]) + collection[1] = 4 + } + + func testArrayFilterUpdateNonPassingToPassing() { + collection.filter { $0 % 2 != 0 }.skip(1).expectNext([CollectionChangeset(collection: [1, 5, 3], inserts: [1], deletes: [], updates: [])]) + collection[1] = 5 + } + + func testArrayFilterUpdatePassingToPassing() { + collection.filter { $0 % 2 != 0 }.skip(1).expectNext([CollectionChangeset(collection: [1, 5], inserts: [], deletes: [], updates: [1])]) + collection[2] = 5 + } + + func testArrayFilterUpdatePassingToNonPassing() { + collection.filter { $0 % 2 != 0 }.skip(1).expectNext([CollectionChangeset(collection: [1], inserts: [], deletes: [1], updates: [])]) + collection[2] = 4 + } +} + +class DictionaryCollectionPropertyTests: XCTestCase { + + var collection: CollectionProperty<[String: Int]>! + + override func setUp() { + collection = CollectionProperty(["A": 1, "B": 2, "C": 3]) + } + + func testArraySortInsertHighest() { + collection.sort { $0.1 < $1.1 }.skip(1).expectNext([CollectionChangeset(collection: [("A", 1), ("B", 2), ("C", 3), ("X", 4)], inserts: [3], deletes: [], updates: [])]) + collection["X"] = 4 + } + + func testArraySortInsertLowest() { + collection.sort { $0.1 < $1.1 }.skip(1).expectNext([CollectionChangeset(collection: [("X", 0), ("A", 1), ("B", 2), ("C", 3)], inserts: [0], deletes: [], updates: [])]) + collection["X"] = 0 + } + + func testArraySortRemoveHighest() { + collection.sort { $0.1 < $1.1 }.skip(1).expectNext([CollectionChangeset(collection: [("A", 1), ("B", 2)], inserts: [], deletes: [2], updates: [])]) + collection["C"] = nil + } + + func testArraySortRemoveLowest() { + collection.sort { $0.1 < $1.1 }.skip(1).expectNext([CollectionChangeset(collection: [("B", 2), ("C", 3)], inserts: [], deletes: [0], updates: [])]) + collection["A"] = nil + } + + func testArraySortUpdateDoesNotChangePosition() { + collection.sort { $0.1 < $1.1 }.skip(1).expectNext([CollectionChangeset(collection: [("A", 0), ("B", 2), ("C", 3)], inserts: [], deletes: [], updates: [0])]) + collection["A"] = 0 + } + + func testArraySortUpdateDoesNotChangePosition2() { + collection.sort { $0.1 < $1.1 }.skip(1).expectNext([CollectionChangeset(collection: [("A", 1), ("B", 2), ("C", 4)], inserts: [], deletes: [], updates: [2])]) + collection["C"] = 4 + } + + func testArraySortUpdateMovesToFront() { + collection.sort { $0.1 < $1.1 }.skip(1).expectNext([CollectionChangeset(collection: [("B", 0), ("A", 1), ("C", 3)], inserts: [0], deletes: [1], updates: [])]) + collection["B"] = 0 + } + + func testArraySortUpdateMovesToBack() { + collection.sort { $0.1 < $1.1 }.skip(1).expectNext([CollectionChangeset(collection: [("A", 1), ("C", 3), ("B", 4)], inserts: [2], deletes: [1], updates: [])]) + collection["B"] = 4 + } +} diff --git a/ReactiveKitTests/Helpers.swift b/Tests/Helpers.swift similarity index 76% rename from ReactiveKitTests/Helpers.swift rename to Tests/Helpers.swift index 5914f45..d1e7f2d 100644 --- a/ReactiveKitTests/Helpers.swift +++ b/Tests/Helpers.swift @@ -28,8 +28,12 @@ extension EventType { return left == right } else if let left = left as? [String], right = right as? [String] { return left == right + } else if let left = left as? CollectionChangeset<[Int]>, right = right as? CollectionChangeset<[Int]> { + return left.collection == right.collection && left.inserts == right.inserts && left.updates == right.updates && left.deletes == right.deletes + } else if let left = left as? CollectionChangeset<[(String, Int)]>, right = right as? CollectionChangeset<[(String, Int)]> { + return left.collection == right.collection && left.inserts == right.inserts && left.updates == right.updates && left.deletes == right.deletes } else { - fatalError("Cannot compare that element type.") + fatalError("Cannot compare that element type. \(left)") } } else { return false @@ -92,3 +96,14 @@ class Scheduler { } } } + +func ==(lhs: [(String, Int)], rhs: [(String, Int)]) -> Bool { + if lhs.count != rhs.count { + return false + } + + return zip(lhs, rhs).reduce(true) { memo, new in + memo && new.0.0 == new.1.0 && new.0.1 == new.1.1 + } +} + diff --git a/Tests/OperationTests.swift b/Tests/OperationTests.swift index d51920f..b1e98fc 100644 --- a/Tests/OperationTests.swift +++ b/Tests/OperationTests.swift @@ -186,13 +186,13 @@ class OperatorsTests: XCTestCase { takenLast2.expectNext([2, 3]) } - func testThrottle() { - let operation = Operation.interval(0.4, queue: Queue.global).take(5) - let distinct = operation.throttle(1) - let expectation = expectationWithDescription("completed") - distinct.expectNext([0, 3], expectation: expectation) - waitForExpectationsWithTimeout(3, handler: nil) - } +// func testThrottle() { +// let operation = Operation.interval(0.4, queue: Queue.global).take(5) +// let distinct = operation.throttle(1) +// let expectation = expectationWithDescription("completed") +// distinct.expectNext([0, 3], expectation: expectation) +// waitForExpectationsWithTimeout(3, handler: nil) +// } func testIgnoreNil() { let operation = Operation.sequence(Array([1, nil, 3])) diff --git a/Tests/StreamTests.swift b/Tests/StreamTests.swift index b53a23b..40df9c1 100644 --- a/Tests/StreamTests.swift +++ b/Tests/StreamTests.swift @@ -165,13 +165,13 @@ class StreamTests: XCTestCase { takenLast2.expectNext([2, 3]) } - func testThrottle() { - let stream = Stream.interval(0.4, queue: Queue.global).take(5) - let distinct = stream.throttle(1) - let expectation = expectationWithDescription("completed") - distinct.expectNext([0, 3], expectation: expectation) - waitForExpectationsWithTimeout(3, handler: nil) - } +// func testThrottle() { +// let stream = Stream.interval(0.4, queue: Queue.global).take(5) +// let distinct = stream.throttle(1) +// let expectation = expectationWithDescription("completed") +// distinct.expectNext([0, 3], expectation: expectation) +// waitForExpectationsWithTimeout(3, handler: nil) +// } func testIgnoreNil() { let stream = Stream.sequence(Array([1, nil, 3]))