Skip to content
This repository was archived by the owner on May 26, 2020. It is now read-only.

Commit 1501e01

Browse files
authored
Replace Delta with Snapshot and Changeset. (#33)
* Snapshot and Changeset. * Documentation and reproducibility tests.
1 parent a985798 commit 1501e01

9 files changed

Lines changed: 844 additions & 304 deletions

File tree

ReactiveCollections.xcodeproj/project.pbxproj

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,17 @@
4444
7DE06DDF1DFADD9B003303AB /* ReactiveCollections.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7DE06DBD1DFADCAE003303AB /* ReactiveCollections.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
4545
7DE06DE01DFADDA0003303AB /* ReactiveSwift.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7DE06DD41DFADCE4003303AB /* ReactiveSwift.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
4646
7DE06DE11DFADDA0003303AB /* Result.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7DE06DD51DFADCE4003303AB /* Result.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
47-
7DF60EED1E007DEF0096283B /* Delta.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DF60EEC1E007DEF0096283B /* Delta.swift */; };
48-
7DF60EEE1E007DEF0096283B /* Delta.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DF60EEC1E007DEF0096283B /* Delta.swift */; };
49-
7DF60EEF1E007DEF0096283B /* Delta.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DF60EEC1E007DEF0096283B /* Delta.swift */; };
50-
7DF60EF01E007DEF0096283B /* Delta.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DF60EEC1E007DEF0096283B /* Delta.swift */; };
47+
9A1BD5C61F3AD6A80087EE41 /* ChangesetSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A1BD5C51F3AD6A80087EE41 /* ChangesetSpec.swift */; };
48+
9A1BD5C71F3AD6A80087EE41 /* ChangesetSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A1BD5C51F3AD6A80087EE41 /* ChangesetSpec.swift */; };
49+
9A1BD5C81F3AD6A80087EE41 /* ChangesetSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A1BD5C51F3AD6A80087EE41 /* ChangesetSpec.swift */; };
50+
9AD6D9EB1F2DEFCB00F0BC11 /* Changeset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AD6D9E51F2DEFCB00F0BC11 /* Changeset.swift */; };
51+
9AD6D9EC1F2DEFCB00F0BC11 /* Changeset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AD6D9E51F2DEFCB00F0BC11 /* Changeset.swift */; };
52+
9AD6D9ED1F2DEFCB00F0BC11 /* Changeset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AD6D9E51F2DEFCB00F0BC11 /* Changeset.swift */; };
53+
9AD6D9EE1F2DEFCB00F0BC11 /* Changeset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AD6D9E51F2DEFCB00F0BC11 /* Changeset.swift */; };
54+
9AD6D9EF1F2DEFCB00F0BC11 /* Snapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AD6D9E61F2DEFCB00F0BC11 /* Snapshot.swift */; };
55+
9AD6D9F01F2DEFCB00F0BC11 /* Snapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AD6D9E61F2DEFCB00F0BC11 /* Snapshot.swift */; };
56+
9AD6D9F11F2DEFCB00F0BC11 /* Snapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AD6D9E61F2DEFCB00F0BC11 /* Snapshot.swift */; };
57+
9AD6D9F21F2DEFCB00F0BC11 /* Snapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AD6D9E61F2DEFCB00F0BC11 /* Snapshot.swift */; };
5158
9AF7E1861E9A8CB000F6672B /* Nimble.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9AF7E1841E9A8C9F00F6672B /* Nimble.framework */; };
5259
9AF7E1871E9A8CB000F6672B /* Quick.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9AF7E1851E9A8C9F00F6672B /* Quick.framework */; };
5360
9AF7E1881E9A8CB400F6672B /* Nimble.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 9AF7E1841E9A8C9F00F6672B /* Nimble.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
@@ -154,8 +161,10 @@
154161
7DE06DC51DFADCAF003303AB /* ReactiveCollectionsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ReactiveCollectionsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
155162
7DE06DD41DFADCE4003303AB /* ReactiveSwift.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ReactiveSwift.framework; path = Carthage/Build/tvOS/ReactiveSwift.framework; sourceTree = "<group>"; };
156163
7DE06DD51DFADCE4003303AB /* Result.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Result.framework; path = Carthage/Build/tvOS/Result.framework; sourceTree = "<group>"; };
157-
7DF60EEC1E007DEF0096283B /* Delta.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Delta.swift; sourceTree = "<group>"; };
164+
9A1BD5C51F3AD6A80087EE41 /* ChangesetSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ChangesetSpec.swift; path = ReactiveCollectionsTests/ChangesetSpec.swift; sourceTree = "<group>"; };
158165
9A37DC651E0B34D70075EC2E /* ReactiveArraySpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ReactiveArraySpec.swift; path = ReactiveCollectionsTests/ReactiveArraySpec.swift; sourceTree = "<group>"; };
166+
9AD6D9E51F2DEFCB00F0BC11 /* Changeset.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Changeset.swift; sourceTree = "<group>"; };
167+
9AD6D9E61F2DEFCB00F0BC11 /* Snapshot.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Snapshot.swift; sourceTree = "<group>"; };
159168
9AF7E17E1E9A8BE100F6672B /* LinuxMain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinuxMain.swift; sourceTree = "<group>"; };
160169
9AF7E1801E9A8C8A00F6672B /* Nimble.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Nimble.framework; path = Carthage/Build/tvOS/Nimble.framework; sourceTree = "<group>"; };
161170
9AF7E1811E9A8C8A00F6672B /* Quick.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Quick.framework; path = Carthage/Build/tvOS/Quick.framework; sourceTree = "<group>"; };
@@ -314,7 +323,8 @@
314323
isa = PBXGroup;
315324
children = (
316325
7D69AAF71DF9D07800FCB568 /* ReactiveArray.swift */,
317-
7DF60EEC1E007DEF0096283B /* Delta.swift */,
326+
9AD6D9E51F2DEFCB00F0BC11 /* Changeset.swift */,
327+
9AD6D9E61F2DEFCB00F0BC11 /* Snapshot.swift */,
318328
7D3D8BE41DF9EAE000E90921 /* Supporting Files */,
319329
);
320330
name = ReactiveCollections;
@@ -325,6 +335,7 @@
325335
isa = PBXGroup;
326336
children = (
327337
9AF7E17E1E9A8BE100F6672B /* LinuxMain.swift */,
338+
9A1BD5C51F3AD6A80087EE41 /* ChangesetSpec.swift */,
328339
9A37DC651E0B34D70075EC2E /* ReactiveArraySpec.swift */,
329340
9AF7E19C1E9C9EAC00F6672B /* Delta+NimbleMatcher.swift */,
330341
7D3D8BE51DF9EB1D00E90921 /* Supporting Files */,
@@ -767,8 +778,9 @@
767778
isa = PBXSourcesBuildPhase;
768779
buildActionMask = 2147483647;
769780
files = (
781+
9AD6D9EB1F2DEFCB00F0BC11 /* Changeset.swift in Sources */,
770782
7D69AAF81DF9D07800FCB568 /* ReactiveArray.swift in Sources */,
771-
7DF60EED1E007DEF0096283B /* Delta.swift in Sources */,
783+
9AD6D9EF1F2DEFCB00F0BC11 /* Snapshot.swift in Sources */,
772784
);
773785
runOnlyForDeploymentPostprocessing = 0;
774786
};
@@ -778,24 +790,27 @@
778790
files = (
779791
9AF7E19D1E9C9EAC00F6672B /* Delta+NimbleMatcher.swift in Sources */,
780792
7D85106C1E0BDFC200A57CC9 /* ReactiveArraySpec.swift in Sources */,
793+
9A1BD5C61F3AD6A80087EE41 /* ChangesetSpec.swift in Sources */,
781794
);
782795
runOnlyForDeploymentPostprocessing = 0;
783796
};
784797
7DC1E2CC1DFADEA400A61745 /* Sources */ = {
785798
isa = PBXSourcesBuildPhase;
786799
buildActionMask = 2147483647;
787800
files = (
801+
9AD6D9EE1F2DEFCB00F0BC11 /* Changeset.swift in Sources */,
788802
7DC1E2DA1DFADF9B00A61745 /* ReactiveArray.swift in Sources */,
789-
7DF60EF01E007DEF0096283B /* Delta.swift in Sources */,
803+
9AD6D9F21F2DEFCB00F0BC11 /* Snapshot.swift in Sources */,
790804
);
791805
runOnlyForDeploymentPostprocessing = 0;
792806
};
793807
7DE06D8E1DFADA84003303AB /* Sources */ = {
794808
isa = PBXSourcesBuildPhase;
795809
buildActionMask = 2147483647;
796810
files = (
811+
9AD6D9EC1F2DEFCB00F0BC11 /* Changeset.swift in Sources */,
797812
7DE06DAB1DFADB0F003303AB /* ReactiveArray.swift in Sources */,
798-
7DF60EEE1E007DEF0096283B /* Delta.swift in Sources */,
813+
9AD6D9F01F2DEFCB00F0BC11 /* Snapshot.swift in Sources */,
799814
);
800815
runOnlyForDeploymentPostprocessing = 0;
801816
};
@@ -805,15 +820,17 @@
805820
files = (
806821
9AF7E19E1E9C9EAC00F6672B /* Delta+NimbleMatcher.swift in Sources */,
807822
7D85106D1E0BDFCD00A57CC9 /* ReactiveArraySpec.swift in Sources */,
823+
9A1BD5C71F3AD6A80087EE41 /* ChangesetSpec.swift in Sources */,
808824
);
809825
runOnlyForDeploymentPostprocessing = 0;
810826
};
811827
7DE06DB81DFADCAE003303AB /* Sources */ = {
812828
isa = PBXSourcesBuildPhase;
813829
buildActionMask = 2147483647;
814830
files = (
831+
9AD6D9ED1F2DEFCB00F0BC11 /* Changeset.swift in Sources */,
815832
7DE06DDA1DFADD69003303AB /* ReactiveArray.swift in Sources */,
816-
7DF60EEF1E007DEF0096283B /* Delta.swift in Sources */,
833+
9AD6D9F11F2DEFCB00F0BC11 /* Snapshot.swift in Sources */,
817834
);
818835
runOnlyForDeploymentPostprocessing = 0;
819836
};
@@ -823,6 +840,7 @@
823840
files = (
824841
9AF7E19F1E9C9EAC00F6672B /* Delta+NimbleMatcher.swift in Sources */,
825842
7D85106E1E0BDFD400A57CC9 /* ReactiveArraySpec.swift in Sources */,
843+
9A1BD5C81F3AD6A80087EE41 /* ChangesetSpec.swift in Sources */,
826844
);
827845
runOnlyForDeploymentPostprocessing = 0;
828846
};

Sources/Changeset.swift

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import Foundation
2+
3+
/// Represents an atomic batch of changes made to a collection.
4+
///
5+
/// A `Changeset` represents changes as **offsets** of elements. You may
6+
/// subscript a collection of zero-based indexing with the offsets, e.g. `Array`. You must
7+
/// otherwise convert the offsets into indices before subscripting.
8+
///
9+
/// # Implicit order between offsets.
10+
///
11+
/// Removal offsets precede insertion offsets. Move offset pairs are semantically
12+
/// equivalent to a pair of removal offset (source) and an insertion offset (destination).
13+
///
14+
/// ## Example: Reproducing an array.
15+
///
16+
/// Given a previous version of a collection, a current version of a collection, and a
17+
/// `Changeset`, we can reproduce the current version by applying the `Changeset` to
18+
/// the previous version.
19+
///
20+
/// - note: `Array` is a zero-based container, thus being able to consume zero-based
21+
/// offsets directly. If you are working upon non-zero-based or undetermined collection
22+
/// types, you **must** first convert offsets into indices.
23+
///
24+
/// 1. Clone the previous version.
25+
///
26+
/// ```
27+
/// var elements = previous
28+
/// ```
29+
///
30+
/// 2. Copy mutated elements specified by `mutations`.
31+
///
32+
/// `mutations` offsets are invariant. So it can simply be conducted as:
33+
/// ```
34+
/// for range in changeset.mutations.rangeView {
35+
/// elements[range] = current[range]
36+
/// }
37+
/// ```
38+
///
39+
/// 3. Perform removals specified by `removals` and `moves` (sources).
40+
///
41+
/// ```
42+
/// let removals = changeset.removals
43+
/// .union(IndexSet(changeset.moves.map { $0.source }))
44+
///
45+
/// for range in removals.rangeView.reversed() {
46+
/// elements.removeSubrange(range)
47+
/// }
48+
/// ```
49+
///
50+
/// 4. Perform inserts specified by `inserts` and `moves` (destinations).
51+
///
52+
/// ```
53+
/// let inserts = changeset.inserts
54+
/// .union(IndexSet(changeset.moves.map { $0.destination }))
55+
///
56+
/// for range in inserts.rangeView {
57+
/// elements.insert(contentsOf: current[range], at: range.lowerBound)
58+
/// }
59+
/// ```
60+
///
61+
public struct Changeset {
62+
/// Represents the context of a move operation applied to a collection.
63+
public struct Move {
64+
public let source: Int
65+
public let destination: Int
66+
public let isMutated: Bool
67+
68+
public init(source: Int, destination: Int, isMutated: Bool) {
69+
(self.source, self.destination, self.isMutated) = (source, destination, isMutated)
70+
}
71+
}
72+
73+
/// The offsets of inserted elements in the current version of the collection.
74+
///
75+
/// - important: To obtain the actual index, you must apply
76+
/// `Collection.index(self:_:offsetBy:)` on the current version, the
77+
/// start index and the offset.
78+
public var inserts = IndexSet()
79+
80+
/// The offsets of removed elements in the previous version of the collection.
81+
///
82+
/// - important: To obtain the actual index, you must apply
83+
/// `Collection.index(self:_:offsetBy:)` on the previous version, the
84+
/// start index and the offset.
85+
public var removals = IndexSet()
86+
87+
/// The offsets of position-invariant mutations that are valid across both versions
88+
/// of the collection.
89+
///
90+
/// `mutations` only implies an invariant relative position. The actual indexes can
91+
/// be different, depending on the collection type.
92+
///
93+
/// If an element has both changed and moved, it is instead included in `moves` with
94+
/// an asserted mutation flag.
95+
///
96+
/// - important: To obtain the actual index, you must apply
97+
/// `Collection.index(self:_:offsetBy:)` on the relevant versions, the
98+
/// start index and the offset.
99+
public var mutations = IndexSet()
100+
101+
/// The offset pairs of moves with a mutation flag as the associated value.
102+
///
103+
/// The source offset is semantically equivalent to a removal offset, while the
104+
/// destination offset is semantically equivalent to an insertion offset.
105+
///
106+
/// - important: To obtain the actual index, you must apply
107+
/// `Collection.index(self:_:offsetBy:)` on the relevant versions, the
108+
/// start index and the offset.
109+
public var moves = [Move]()
110+
111+
public init() {}
112+
113+
public init(inserts: IndexSet = [], removals: IndexSet = [], mutations: IndexSet = [], moves: [Move] = []) {
114+
(self.inserts, self.removals) = (inserts, removals)
115+
(self.mutations, self.moves) = (mutations, moves)
116+
}
117+
118+
public init<C: Collection>(initial: C) {
119+
inserts = IndexSet(integersIn: 0 ..< Int(initial.count))
120+
}
121+
}
122+
123+
#if !swift(>=3.2)
124+
extension SignedInteger {
125+
fileprivate init<I: SignedInteger>(_ integer: I) {
126+
self.init(integer.toIntMax())
127+
}
128+
}
129+
#endif
130+
131+
extension Changeset.Move: Equatable {
132+
public static func == (left: Changeset.Move, right: Changeset.Move) -> Bool {
133+
return left.isMutated == right.isMutated && left.source == right.source && left.destination == right.destination
134+
}
135+
}
136+
137+
extension Changeset: Equatable {
138+
public static func == (left: Changeset, right: Changeset) -> Bool {
139+
return left.inserts == right.inserts && left.removals == right.removals && left.mutations == right.mutations && left.moves == right.moves
140+
}
141+
}
142+
143+
// Better debugging experience
144+
extension Changeset: CustomDebugStringConvertible {
145+
public var debugDescription: String {
146+
func moveDescription(_ move: Move) -> String {
147+
return "\(move.source) -> \(move.isMutated ? "*" : "")\(move.destination)"
148+
}
149+
150+
return ([
151+
"- inserted \(inserts.count) item(s) at [\(inserts.map(String.init).joined(separator: ", "))]" as String,
152+
"- deleted \(removals.count) item(s) at [\(removals.map(String.init).joined(separator: ", "))]" as String,
153+
"- mutated \(mutations.count) item(s) at [\(mutations.map(String.init).joined(separator: ", "))]" as String,
154+
"- moved \(moves.count) item(s) at [\(moves.map(moveDescription).joined(separator: ", "))]" as String,
155+
] as [String]).joined(separator: "\n")
156+
}
157+
}

Sources/Delta.swift

Lines changed: 0 additions & 39 deletions
This file was deleted.

0 commit comments

Comments
 (0)