diff options
Diffstat (limited to 'subprojects/viatra-runtime/src/main/java/tools/refinery/viatra/runtime/matchers/util/TimelyMemory.java')
-rw-r--r-- | subprojects/viatra-runtime/src/main/java/tools/refinery/viatra/runtime/matchers/util/TimelyMemory.java | 517 |
1 files changed, 517 insertions, 0 deletions
diff --git a/subprojects/viatra-runtime/src/main/java/tools/refinery/viatra/runtime/matchers/util/TimelyMemory.java b/subprojects/viatra-runtime/src/main/java/tools/refinery/viatra/runtime/matchers/util/TimelyMemory.java new file mode 100644 index 00000000..90fcad4d --- /dev/null +++ b/subprojects/viatra-runtime/src/main/java/tools/refinery/viatra/runtime/matchers/util/TimelyMemory.java | |||
@@ -0,0 +1,517 @@ | |||
1 | /******************************************************************************* | ||
2 | * Copyright (c) 2010-2019, Tamas Szabo, itemis AG, Gabor Bergmann, IncQuery Labs Ltd. | ||
3 | * This program and the accompanying materials are made available under the | ||
4 | * terms of the Eclipse Public License v. 2.0 which is available at | ||
5 | * http://www.eclipse.org/legal/epl-v20.html. | ||
6 | * | ||
7 | * SPDX-License-Identifier: EPL-2.0 | ||
8 | *******************************************************************************/ | ||
9 | package tools.refinery.viatra.runtime.matchers.util; | ||
10 | |||
11 | import java.util.Collections; | ||
12 | import java.util.Map; | ||
13 | import java.util.Map.Entry; | ||
14 | import java.util.NavigableMap; | ||
15 | import java.util.Set; | ||
16 | import java.util.TreeMap; | ||
17 | |||
18 | import tools.refinery.viatra.runtime.matchers.tuple.ITuple; | ||
19 | import tools.refinery.viatra.runtime.matchers.tuple.Tuple; | ||
20 | import tools.refinery.viatra.runtime.matchers.util.resumable.Resumable; | ||
21 | import tools.refinery.viatra.runtime.matchers.util.resumable.UnmaskedResumable; | ||
22 | import tools.refinery.viatra.runtime.matchers.util.timeline.Diff; | ||
23 | import tools.refinery.viatra.runtime.matchers.util.timeline.Timeline; | ||
24 | import tools.refinery.viatra.runtime.matchers.util.timeline.Timelines; | ||
25 | |||
26 | /** | ||
27 | * A timely memory implementation that incrementally maintains the {@link Timeline}s of tuples. The memory is capable of | ||
28 | * lazy folding (see {@link Resumable}). | ||
29 | * | ||
30 | * @author Tamas Szabo | ||
31 | * @since 2.3 | ||
32 | */ | ||
33 | public class TimelyMemory<Timestamp extends Comparable<Timestamp>> implements Clearable, UnmaskedResumable<Timestamp> { | ||
34 | |||
35 | protected final Map<Tuple, TreeMap<Timestamp, CumulativeCounter>> counters; | ||
36 | protected final Map<Tuple, Timeline<Timestamp>> timelines; | ||
37 | public final TreeMap<Timestamp, Map<Tuple, FoldingState>> foldingState; | ||
38 | protected final Set<Tuple> presentAtInfinity; | ||
39 | protected final boolean isLazy; | ||
40 | protected final Diff<Timestamp> EMPTY_DIFF = new Diff<Timestamp>(); | ||
41 | |||
42 | public TimelyMemory() { | ||
43 | this(false); | ||
44 | } | ||
45 | |||
46 | public TimelyMemory(final boolean isLazy) { | ||
47 | this.counters = CollectionsFactory.createMap(); | ||
48 | this.timelines = CollectionsFactory.createMap(); | ||
49 | this.presentAtInfinity = CollectionsFactory.createSet(); | ||
50 | this.isLazy = isLazy; | ||
51 | if (isLazy) { | ||
52 | this.foldingState = CollectionsFactory.createTreeMap(); | ||
53 | } else { | ||
54 | this.foldingState = null; | ||
55 | } | ||
56 | } | ||
57 | |||
58 | @Override | ||
59 | public Set<Tuple> getResumableTuples() { | ||
60 | if (this.foldingState == null || this.foldingState.isEmpty()) { | ||
61 | return Collections.emptySet(); | ||
62 | } else { | ||
63 | return this.foldingState.firstEntry().getValue().keySet(); | ||
64 | } | ||
65 | } | ||
66 | |||
67 | @Override | ||
68 | public Timestamp getResumableTimestamp() { | ||
69 | if (this.foldingState == null || this.foldingState.isEmpty()) { | ||
70 | return null; | ||
71 | } else { | ||
72 | return this.foldingState.firstKey(); | ||
73 | } | ||
74 | } | ||
75 | |||
76 | /** | ||
77 | * Registers the given folding state for the specified timestamp and tuple. If there is already a state stored, the | ||
78 | * two states will be merged together. | ||
79 | */ | ||
80 | protected void addFoldingState(final Tuple tuple, final FoldingState state, final Timestamp timestamp) { | ||
81 | assert state.diff != 0; | ||
82 | final Map<Tuple, FoldingState> tupleMap = this.foldingState.computeIfAbsent(timestamp, | ||
83 | k -> CollectionsFactory.createMap()); | ||
84 | tupleMap.compute(tuple, (k, v) -> { | ||
85 | return v == null ? state : v.merge(state); | ||
86 | }); | ||
87 | } | ||
88 | |||
89 | @Override | ||
90 | public Map<Tuple, Diff<Timestamp>> resumeAt(final Timestamp timestamp) { | ||
91 | Timestamp current = this.getResumableTimestamp(); | ||
92 | if (current == null) { | ||
93 | throw new IllegalStateException("There is othing to fold!"); | ||
94 | } else if (current.compareTo(timestamp) != 0) { | ||
95 | // It can happen that already registered folding states end up having zero diffs, | ||
96 | // and we are instructed to continue folding at a timestamp that is higher | ||
97 | // than the lowest timestamp with a folding state. | ||
98 | // However, we only do garbage collection in doFoldingState, so now it is time to | ||
99 | // first clean up those states with zero diffs. | ||
100 | while (current != null && current.compareTo(timestamp) < 0) { | ||
101 | final Map<Tuple, FoldingState> tupleMap = this.foldingState.remove(current); | ||
102 | for (final Entry<Tuple, FoldingState> entry : tupleMap.entrySet()) { | ||
103 | final Tuple key = entry.getKey(); | ||
104 | final FoldingState value = entry.getValue(); | ||
105 | if (value.diff != 0) { | ||
106 | throw new IllegalStateException("Expected zero diff during garbage collection at " + current | ||
107 | + ", but the diff was " + value.diff + "!"); | ||
108 | } | ||
109 | doFoldingStep(key, value, current); | ||
110 | } | ||
111 | current = this.getResumableTimestamp(); | ||
112 | } | ||
113 | if (current == null || current.compareTo(timestamp) != 0) { | ||
114 | throw new IllegalStateException("Expected to continue folding at " + timestamp + "!"); | ||
115 | } | ||
116 | } | ||
117 | |||
118 | final Map<Tuple, Diff<Timestamp>> diffMap = CollectionsFactory.createMap(); | ||
119 | final Map<Tuple, FoldingState> tupleMap = this.foldingState.remove(timestamp); | ||
120 | for (final Entry<Tuple, FoldingState> entry : tupleMap.entrySet()) { | ||
121 | final Tuple key = entry.getKey(); | ||
122 | final FoldingState value = entry.getValue(); | ||
123 | diffMap.put(key, doFoldingStep(key, value, timestamp)); | ||
124 | } | ||
125 | |||
126 | if (this.foldingState.get(timestamp) != null) { | ||
127 | throw new IllegalStateException( | ||
128 | "Folding at " + timestamp + " produced more folding work at the same timestamp!"); | ||
129 | } | ||
130 | |||
131 | return diffMap; | ||
132 | } | ||
133 | |||
134 | protected Diff<Timestamp> doFoldingStep(final Tuple tuple, final FoldingState state, final Timestamp timestamp) { | ||
135 | final CumulativeCounter counter = getCounter(tuple, timestamp); | ||
136 | if (state.diff == 0) { | ||
137 | gcCounters(counter, tuple, timestamp); | ||
138 | return EMPTY_DIFF; | ||
139 | } else { | ||
140 | final Diff<Timestamp> resultDiff = new Diff<>(); | ||
141 | final Timestamp nextTimestamp = this.counters.get(tuple).higherKey(timestamp); | ||
142 | |||
143 | final int oldCumulative = counter.cumulative; | ||
144 | |||
145 | counter.cumulative += state.diff; | ||
146 | |||
147 | computeDiffsLazy(state.diff < 0 ? Direction.DELETE : Direction.INSERT, oldCumulative, counter.cumulative, | ||
148 | timestamp, nextTimestamp, resultDiff); | ||
149 | |||
150 | gcCounters(counter, tuple, timestamp); | ||
151 | updateTimeline(tuple, resultDiff); | ||
152 | |||
153 | // prepare folding state for next timestamp | ||
154 | if (nextTimestamp != null) { | ||
155 | // propagate the incoming diff, not the diff stored in counter | ||
156 | addFoldingState(tuple, new FoldingState(state.diff), nextTimestamp); | ||
157 | } | ||
158 | |||
159 | return resultDiff; | ||
160 | } | ||
161 | } | ||
162 | |||
163 | /** | ||
164 | * On-demand initializes and returns the counter for the given tuple and timestamp. | ||
165 | */ | ||
166 | protected CumulativeCounter getCounter(final Tuple tuple, final Timestamp timestamp) { | ||
167 | final TreeMap<Timestamp, CumulativeCounter> counterTimeline = this.counters.computeIfAbsent(tuple, | ||
168 | k -> CollectionsFactory.createTreeMap()); | ||
169 | |||
170 | final CumulativeCounter counter = counterTimeline.computeIfAbsent(timestamp, k -> { | ||
171 | final Entry<Timestamp, CumulativeCounter> previousCounter = counterTimeline.lowerEntry(k); | ||
172 | final int previousCumulative = previousCounter == null ? 0 : previousCounter.getValue().cumulative; | ||
173 | return new CumulativeCounter(0, previousCumulative); | ||
174 | }); | ||
175 | |||
176 | return counter; | ||
177 | } | ||
178 | |||
179 | /** | ||
180 | * Garbage collects the counter of the given tuple and timestamp if the new diff is zero. | ||
181 | */ | ||
182 | protected void gcCounters(final CumulativeCounter counter, final Tuple tuple, final Timestamp timestamp) { | ||
183 | if (counter.diff == 0) { | ||
184 | final TreeMap<Timestamp, CumulativeCounter> counterMap = this.counters.get(tuple); | ||
185 | counterMap.remove(timestamp); | ||
186 | if (counterMap.isEmpty()) { | ||
187 | this.counters.remove(tuple); | ||
188 | } | ||
189 | } | ||
190 | } | ||
191 | |||
192 | /** | ||
193 | * Utility method that computes the timeline diffs in case of lazy memories. The diffs will be inserted into the | ||
194 | * input parameter. This method computes diffs for entire plateaus that spans from timestamp to nextTimestamp. | ||
195 | * | ||
196 | * Compared to the eager version of this method, the lazy version makes use of both the old and the new cumulative | ||
197 | * values because it can happen that the cumulative is incremented by a value that is larger than 1 (as folding | ||
198 | * states are merged together). This means that we cant decide whether the cumulative became positive by comparing | ||
199 | * the new value to 1. | ||
200 | */ | ||
201 | protected void computeDiffsLazy(final Direction direction, final int oldCumulative, final int newCumulative, | ||
202 | final Timestamp timestamp, final Timestamp nextTimestamp, final Diff<Timestamp> diffs) { | ||
203 | if (direction == Direction.INSERT) { | ||
204 | if (newCumulative == 0) { | ||
205 | throw new IllegalStateException("Cumulative count can never be negative!"); | ||
206 | } else { | ||
207 | if (oldCumulative == 0 /* current became positive */) { | ||
208 | // (1) either we sent out a DELETE before and now we need to cancel it, | ||
209 | // (2) or we just INSERT this for the first time | ||
210 | diffs.add(new Signed<>(Direction.INSERT, timestamp)); | ||
211 | if (nextTimestamp != null) { | ||
212 | diffs.add(new Signed<>(Direction.DELETE, nextTimestamp)); | ||
213 | } | ||
214 | } else /* current stays positive */ { | ||
215 | // nothing to do | ||
216 | } | ||
217 | } | ||
218 | } else { | ||
219 | if (newCumulative < 0) { | ||
220 | throw new IllegalStateException("Cumulative count can never be negative!"); | ||
221 | } else { | ||
222 | if (newCumulative == 0 /* current became zero */) { | ||
223 | diffs.add(new Signed<>(Direction.DELETE, timestamp)); | ||
224 | if (nextTimestamp != null) { | ||
225 | diffs.add(new Signed<>(Direction.INSERT, nextTimestamp)); | ||
226 | } | ||
227 | } else /* current stays positive */ { | ||
228 | // nothing to do | ||
229 | } | ||
230 | } | ||
231 | } | ||
232 | } | ||
233 | |||
234 | /** | ||
235 | * Utility method that computes the timeline diffs in case of eager memories. The diffs will be inserted into the | ||
236 | * input parameter. This method computes diffs that describe momentary changes instead of plateaus. Returns a | ||
237 | * {@link SignChange} that describes how the sign has changed at the given timestamp. | ||
238 | */ | ||
239 | protected SignChange computeDiffsEager(final Direction direction, final CumulativeCounter counter, | ||
240 | final SignChange signChangeAtPrevious, final Timestamp timestamp, final Diff<Timestamp> diffs) { | ||
241 | if (direction == Direction.INSERT) { | ||
242 | if (counter.cumulative == 0) { | ||
243 | throw new IllegalStateException("Cumulative count can never be negative!"); | ||
244 | } else { | ||
245 | if (counter.cumulative == 1 /* current became positive */) { | ||
246 | if (signChangeAtPrevious != SignChange.BECAME_POSITIVE) { | ||
247 | // (1) either we sent out a DELETE before and now we need to cancel it, | ||
248 | // (2) or we just INSERT this for the first time | ||
249 | diffs.add(new Signed<>(Direction.INSERT, timestamp)); | ||
250 | } else { | ||
251 | // we have already emitted this at the previous timestamp | ||
252 | // both previous and current became positive | ||
253 | throw new IllegalStateException( | ||
254 | "This would mean that the diff at current is 0 " + counter.diff); | ||
255 | } | ||
256 | |||
257 | // remember for next timestamp | ||
258 | return SignChange.BECAME_POSITIVE; | ||
259 | } else /* current stays positive */ { | ||
260 | if (signChangeAtPrevious == SignChange.BECAME_POSITIVE) { | ||
261 | // we sent out an INSERT before and now the timeline is positive already starting at previous | ||
262 | // we need to cancel the effect of this with a DELETE | ||
263 | diffs.add(new Signed<>(Direction.DELETE, timestamp)); | ||
264 | } else { | ||
265 | // this is normal, both previous and current was positive and stays positive | ||
266 | } | ||
267 | |||
268 | // remember for next timestamp | ||
269 | return SignChange.IRRELEVANT; | ||
270 | } | ||
271 | } | ||
272 | } else { | ||
273 | if (counter.cumulative < 0) { | ||
274 | throw new IllegalStateException("Cumulative count can never be negative!"); | ||
275 | } else { | ||
276 | if (counter.cumulative == 0 /* current became zero */) { | ||
277 | if (signChangeAtPrevious != SignChange.BECAME_ZERO) { | ||
278 | // (1) either we sent out a INSERT before and now we need to cancel it, | ||
279 | // (2) or we just DELETE this for the first time | ||
280 | diffs.add(new Signed<>(Direction.DELETE, timestamp)); | ||
281 | } else { | ||
282 | // we have already emitted this at the previous timestamp | ||
283 | // both previous and current became zero | ||
284 | throw new IllegalStateException( | ||
285 | "This would mean that the diff at current is 0 " + counter.diff); | ||
286 | } | ||
287 | |||
288 | // remember for next timestamp | ||
289 | return SignChange.BECAME_ZERO; | ||
290 | } else /* current stays positive */ { | ||
291 | if (signChangeAtPrevious == SignChange.BECAME_ZERO) { | ||
292 | // we sent out a DELETE before and now the timeline is zero already starting at previous | ||
293 | // we need to cancel the effect of this with a INSERT | ||
294 | diffs.add(new Signed<>(Direction.INSERT, timestamp)); | ||
295 | } else { | ||
296 | // this is normal, both previous and current was positive and stays positive | ||
297 | } | ||
298 | |||
299 | // remember for next timestamp | ||
300 | return SignChange.IRRELEVANT; | ||
301 | } | ||
302 | } | ||
303 | } | ||
304 | } | ||
305 | |||
306 | public Diff<Timestamp> put(final Tuple tuple, final Timestamp timestamp) { | ||
307 | if (this.isLazy) { | ||
308 | return putLazy(tuple, timestamp); | ||
309 | } else { | ||
310 | return putEager(tuple, timestamp); | ||
311 | } | ||
312 | } | ||
313 | |||
314 | public Diff<Timestamp> remove(final Tuple tuple, final Timestamp timestamp) { | ||
315 | if (this.isLazy) { | ||
316 | return removeLazy(tuple, timestamp); | ||
317 | } else { | ||
318 | return removeEager(tuple, timestamp); | ||
319 | } | ||
320 | } | ||
321 | |||
322 | protected Diff<Timestamp> putEager(final Tuple tuple, final Timestamp timestamp) { | ||
323 | final Diff<Timestamp> resultDiff = new Diff<>(); | ||
324 | final CumulativeCounter counter = getCounter(tuple, timestamp); | ||
325 | ++counter.diff; | ||
326 | |||
327 | // before the INSERT timestamp, no change at all | ||
328 | // it cannot happen that those became positive in this round | ||
329 | SignChange signChangeAtPrevious = SignChange.IRRELEVANT; | ||
330 | |||
331 | final NavigableMap<Timestamp, CumulativeCounter> nextCounters = this.counters.get(tuple).tailMap(timestamp, | ||
332 | true); | ||
333 | for (final Entry<Timestamp, CumulativeCounter> currentEntry : nextCounters.entrySet()) { | ||
334 | final Timestamp currentTimestamp = currentEntry.getKey(); | ||
335 | final CumulativeCounter currentCounter = currentEntry.getValue(); | ||
336 | ++currentCounter.cumulative; | ||
337 | signChangeAtPrevious = computeDiffsEager(Direction.INSERT, currentCounter, signChangeAtPrevious, | ||
338 | currentTimestamp, resultDiff); | ||
339 | } | ||
340 | |||
341 | gcCounters(counter, tuple, timestamp); | ||
342 | updateTimeline(tuple, resultDiff); | ||
343 | |||
344 | return resultDiff; | ||
345 | } | ||
346 | |||
347 | protected Diff<Timestamp> putLazy(final Tuple tuple, final Timestamp timestamp) { | ||
348 | final CumulativeCounter counter = getCounter(tuple, timestamp); | ||
349 | counter.diff += 1; | ||
350 | // before the INSERT timestamp, no change at all | ||
351 | // it cannot happen that those became positive in this round | ||
352 | addFoldingState(tuple, new FoldingState(+1), timestamp); | ||
353 | return EMPTY_DIFF; | ||
354 | } | ||
355 | |||
356 | protected Diff<Timestamp> removeEager(final Tuple tuple, final Timestamp timestamp) { | ||
357 | final Diff<Timestamp> resultDiff = new Diff<>(); | ||
358 | final CumulativeCounter counter = getCounter(tuple, timestamp); | ||
359 | --counter.diff; | ||
360 | |||
361 | // before the DELETE timestamp, no change at all | ||
362 | // it cannot happen that those became zero in this round | ||
363 | SignChange signChangeAtPrevious = SignChange.IRRELEVANT; | ||
364 | |||
365 | final NavigableMap<Timestamp, CumulativeCounter> nextCounters = this.counters.get(tuple).tailMap(timestamp, | ||
366 | true); | ||
367 | for (final Entry<Timestamp, CumulativeCounter> currentEntry : nextCounters.entrySet()) { | ||
368 | final Timestamp currentTimestamp = currentEntry.getKey(); | ||
369 | final CumulativeCounter currentCounter = currentEntry.getValue(); | ||
370 | --currentCounter.cumulative; | ||
371 | signChangeAtPrevious = computeDiffsEager(Direction.DELETE, currentCounter, signChangeAtPrevious, | ||
372 | currentTimestamp, resultDiff); | ||
373 | } | ||
374 | |||
375 | gcCounters(counter, tuple, timestamp); | ||
376 | updateTimeline(tuple, resultDiff); | ||
377 | |||
378 | return resultDiff; | ||
379 | } | ||
380 | |||
381 | protected Diff<Timestamp> removeLazy(final Tuple tuple, final Timestamp timestamp) { | ||
382 | final CumulativeCounter counter = getCounter(tuple, timestamp); | ||
383 | counter.diff -= 1; | ||
384 | // before the DELETE timestamp, no change at all | ||
385 | // it cannot happen that those became zero in this round | ||
386 | addFoldingState(tuple, new FoldingState(-1), timestamp); | ||
387 | return EMPTY_DIFF; | ||
388 | } | ||
389 | |||
390 | /** | ||
391 | * Updates and garbage collects the timeline of the given tuple based on the given timeline diff. | ||
392 | */ | ||
393 | protected void updateTimeline(final Tuple tuple, final Diff<Timestamp> diff) { | ||
394 | if (!diff.isEmpty()) { | ||
395 | this.timelines.compute(tuple, (k, oldTimeline) -> { | ||
396 | this.presentAtInfinity.remove(tuple); | ||
397 | final Timeline<Timestamp> timeline = oldTimeline == null ? Timelines.createFrom(diff) | ||
398 | : oldTimeline.mergeAdditive(diff); | ||
399 | if (timeline.isPresentAtInfinity()) { | ||
400 | this.presentAtInfinity.add(tuple); | ||
401 | } | ||
402 | if (timeline.isEmpty()) { | ||
403 | return null; | ||
404 | } else { | ||
405 | return timeline; | ||
406 | } | ||
407 | }); | ||
408 | } | ||
409 | } | ||
410 | |||
411 | /** | ||
412 | * @since 2.8 | ||
413 | */ | ||
414 | public Set<Tuple> getTuplesAtInfinity() { | ||
415 | return this.presentAtInfinity; | ||
416 | } | ||
417 | |||
418 | /** | ||
419 | * Returns the number of tuples that are present at the moment 'infinity'. | ||
420 | */ | ||
421 | public int getCountAtInfinity() { | ||
422 | return this.presentAtInfinity.size(); | ||
423 | } | ||
424 | |||
425 | /** | ||
426 | * Returns true if the given tuple is present at the moment 'infinity'. | ||
427 | */ | ||
428 | public boolean isPresentAtInfinity(final Tuple tuple) { | ||
429 | final Timeline<Timestamp> timeline = this.timelines.get(tuple); | ||
430 | if (timeline == null) { | ||
431 | return false; | ||
432 | } else { | ||
433 | return timeline.isPresentAtInfinity(); | ||
434 | } | ||
435 | } | ||
436 | |||
437 | public boolean isEmpty() { | ||
438 | return this.counters.isEmpty(); | ||
439 | } | ||
440 | |||
441 | public int size() { | ||
442 | return this.counters.size(); | ||
443 | } | ||
444 | |||
445 | public Set<Tuple> keySet() { | ||
446 | return this.counters.keySet(); | ||
447 | } | ||
448 | |||
449 | public Map<Tuple, Timeline<Timestamp>> asMap() { | ||
450 | return this.timelines; | ||
451 | } | ||
452 | |||
453 | public Timeline<Timestamp> get(final ITuple tuple) { | ||
454 | return this.timelines.get(tuple); | ||
455 | } | ||
456 | |||
457 | @Override | ||
458 | public void clear() { | ||
459 | this.counters.clear(); | ||
460 | this.timelines.clear(); | ||
461 | if (this.foldingState != null) { | ||
462 | this.foldingState.clear(); | ||
463 | } | ||
464 | } | ||
465 | |||
466 | public boolean containsKey(final ITuple tuple) { | ||
467 | return this.counters.containsKey(tuple); | ||
468 | } | ||
469 | |||
470 | @Override | ||
471 | public String toString() { | ||
472 | return this.counters + "\n" + this.timelines + "\n" + this.foldingState + "\n"; | ||
473 | } | ||
474 | |||
475 | protected static final class CumulativeCounter { | ||
476 | protected int diff; | ||
477 | protected int cumulative; | ||
478 | |||
479 | protected CumulativeCounter(final int diff, final int cumulative) { | ||
480 | this.diff = diff; | ||
481 | this.cumulative = cumulative; | ||
482 | } | ||
483 | |||
484 | @Override | ||
485 | public String toString() { | ||
486 | return "{diff=" + this.diff + ", cumulative=" + this.cumulative + "}"; | ||
487 | } | ||
488 | |||
489 | } | ||
490 | |||
491 | protected static final class FoldingState { | ||
492 | protected final int diff; | ||
493 | |||
494 | protected FoldingState(final int diff) { | ||
495 | this.diff = diff; | ||
496 | } | ||
497 | |||
498 | @Override | ||
499 | public String toString() { | ||
500 | return "{diff=" + this.diff + "}"; | ||
501 | } | ||
502 | |||
503 | /** | ||
504 | * The returned result will never be null, even if the resulting diff is zero. | ||
505 | */ | ||
506 | public FoldingState merge(final FoldingState that) { | ||
507 | Preconditions.checkArgument(that != null); | ||
508 | return new FoldingState(this.diff + that.diff); | ||
509 | } | ||
510 | |||
511 | } | ||
512 | |||
513 | protected enum SignChange { | ||
514 | BECAME_POSITIVE, BECAME_ZERO, IRRELEVANT; | ||
515 | } | ||
516 | |||
517 | } | ||