Skip to content

Commit 0dabfcd

Browse files
committed
copilot
1 parent 40b36ba commit 0dabfcd

2 files changed

Lines changed: 57 additions & 165 deletions

File tree

core/src/main/java/ai/timefold/solver/core/impl/util/ElementAwareArrayList.java

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
* It uses internal state of the entry to track insertion position of the element.
2121
* When an entry is removed, its slot in the underlying collection is replaced with {@code null} (a gap);
2222
* therefore, the insertion position of later elements isn't changed.
23-
* Gaps are removed (the list is fully compacted) when {@link #forEach(Consumer)} or {@link #add(int, Object)} is called.
23+
* Gaps are removed (the list is fully compacted) when {@link #listIterator()} is called.
2424
* {@link #get(int)} and related index-based operations compact only the prefix up to the requested index.
2525
* This keeps the overhead low while giving us most benefits of an array-backed list.
2626
* <p>
@@ -385,13 +385,8 @@ private ElementAwareListIterator(int startingPosition) {
385385
throw new IndexOutOfBoundsException(
386386
"The index (%d) must be >= 0 and <= size (%d).".formatted(startingPosition, currentSize));
387387
}
388-
if (startingPosition > 0 && gapCount > 0) {
389-
var index = startingPosition - 1;
390-
partialCompact(index);
391-
currentPosition = ((Entry) entries[index]).position + 1;
392-
} else {
393-
currentPosition = startingPosition;
394-
}
388+
// listIterator() compacts before construction ⟹ gapless: logical position == physical position.
389+
currentPosition = startingPosition;
395390
logicalPosition = startingPosition;
396391
}
397392

@@ -471,9 +466,12 @@ public void set(T element) {
471466

472467
@Override
473468
public void add(T element) {
469+
var appending = logicalPosition == size();
474470
ElementAwareArrayList.this.add(logicalPosition, element);
475471
logicalPosition++;
476-
currentPosition = logicalPosition;
472+
// Appended entries land at physical lastElementPosition (may exceed logicalPosition when gaps exist);
473+
// interior inserts land at physical logicalPosition (prefix is compacted by add(int,T)).
474+
currentPosition = appending ? lastElementPosition + 1 : logicalPosition;
477475
lastEntry = null;
478476
}
479477

core/src/test/java/ai/timefold/solver/core/impl/util/ElementAwareArrayListTest.java

Lines changed: 50 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -238,77 +238,6 @@ void forEachCompactsWithTailGaps() {
238238
}
239239
}
240240

241-
@Nested
242-
@DisplayName("Compaction tests")
243-
class CompactionTests {
244-
245-
@Test
246-
@DisplayName("Access compacts the list when gaps exist")
247-
void accessCompactsWithGaps() {
248-
var list = new ElementAwareArrayList<String>();
249-
list.addEntry("first");
250-
var entry2 = list.addEntry("second");
251-
list.addEntry("third");
252-
253-
entry2.remove();
254-
255-
assertThat(list).hasSize(2);
256-
assertThat(list.get(0)).isEqualTo("first");
257-
assertThat(list.get(1)).isEqualTo("third");
258-
}
259-
260-
@Test
261-
@DisplayName("Access returns empty when all elements removed")
262-
void accessWhenAllRemoved() {
263-
var list = new ElementAwareArrayList<String>();
264-
var entry1 = list.addEntry("first");
265-
266-
entry1.remove();
267-
268-
assertThat(list).isEmpty();
269-
}
270-
271-
@Test
272-
@DisplayName("Access compacts with tail gaps")
273-
void accessCompactsWithTailGaps() {
274-
var list = new ElementAwareArrayList<String>();
275-
list.add("first");
276-
list.add("second");
277-
var entry3 = list.addEntry("third");
278-
var entry4 = list.addEntry("fourth");
279-
var entry5 = list.addEntry("fifth");
280-
281-
entry3.remove();
282-
entry4.remove();
283-
entry5.remove();
284-
285-
assertThat(list).hasSize(2);
286-
assertThat(entry3.isRemoved()).isTrue();
287-
assertThat(entry4.isRemoved()).isTrue();
288-
assertThat(entry5.isRemoved()).isTrue();
289-
}
290-
291-
@Test
292-
@DisplayName("addAt with trailing gaps only rotates entry into first suffix gap")
293-
void addAtAfterTrailingGapOnly() {
294-
var list = new ElementAwareArrayList<String>();
295-
var e1 = list.addEntry("a");
296-
var e2 = list.addEntry("b");
297-
var e3 = list.addEntry("c");
298-
var e4 = list.addEntry("d");
299-
300-
e3.remove();
301-
e4.remove();
302-
// Physical: [a, b, null], gapCount=1, size=2 (d trim reduced lastElementPosition)
303-
304-
list.add(1, "x"); // partialCompact(0): no prefix gap; slot 1 non-null → rotate b into gap at slot 2
305-
assertThat(list).hasSize(3);
306-
assertThat(copyUsingForEach(list)).containsExactly("a", "x", "b");
307-
assertThat(e1.toString()).contains("@0");
308-
assertThat(e2.toString()).contains("@2");
309-
}
310-
}
311-
312241
@Nested
313242
@DisplayName("Partial compaction tests")
314243
class PartialCompactionTests {
@@ -675,14 +604,35 @@ void removeNullEntry() {
675604
}
676605

677606
@Test
678-
@DisplayName("iterator visits null elements")
607+
@DisplayName("listIterator visits null elements in forward and backward order")
679608
void iterator() {
680609
var list = new ElementAwareArrayList<@Nullable String>();
681610
list.addEntry(null);
682611
list.addEntry("a");
683612
list.addEntry(null);
684-
var result = copyUsingForEach(list);
685-
assertThat(result).containsExactly(null, "a", null);
613+
614+
// Forward traversal returns null elements.
615+
var it = list.listIterator();
616+
assertThat(it.next()).isNull();
617+
assertThat(it.next()).isEqualTo("a");
618+
assertThat(it.next()).isNull();
619+
assertThat(it.hasNext()).isFalse();
620+
621+
// Backward traversal returns null elements.
622+
assertThat(it.previous()).isNull();
623+
assertThat(it.previous()).isEqualTo("a");
624+
assertThat(it.previous()).isNull();
625+
assertThat(it.hasPrevious()).isFalse();
626+
627+
// remove() creates a gap (Entry == null) adjacent to a live null element (Entry != null, element == null);
628+
// next() must skip the former without skipping the latter.
629+
it = list.listIterator();
630+
assertThat(it.next()).isNull(); // null element at logical 0
631+
it.remove(); // slot 0 becomes a removed gap; live null element remains at slot 2
632+
assertThat(it.next()).isEqualTo("a");
633+
assertThat(it.next()).isNull(); // live null element still reachable
634+
assertThat(it.hasNext()).isFalse();
635+
assertThat(list).containsExactly("a", null);
686636
}
687637

688638
@Test
@@ -1091,25 +1041,6 @@ void backwardIteration() {
10911041
assertThat(it.hasPrevious()).isFalse();
10921042
}
10931043

1094-
@Test
1095-
@DisplayName("forward iteration skips null slots")
1096-
void forwardWithGaps() {
1097-
var list = new ElementAwareArrayList<String>();
1098-
list.add("a");
1099-
var entryB = list.addEntry("b");
1100-
list.add("c");
1101-
var entryD = list.addEntry("d");
1102-
list.add("e");
1103-
entryB.remove();
1104-
entryD.remove();
1105-
1106-
var it = list.listIterator();
1107-
assertThat(it.next()).isEqualTo("a");
1108-
assertThat(it.next()).isEqualTo("c");
1109-
assertThat(it.next()).isEqualTo("e");
1110-
assertThat(it.hasNext()).isFalse();
1111-
}
1112-
11131044
@Test
11141045
@DisplayName("remove after next() removes correct element, adjusts cursor")
11151046
void removeFwd() {
@@ -1195,6 +1126,30 @@ void addWithGaps() {
11951126
assertThat(list).containsExactly("a", "x", "c");
11961127
}
11971128

1129+
@Test
1130+
@DisplayName("add() appending to list made gappy mid-iteration positions cursor for previous()")
1131+
void addAtEndAfterMidIterationRemoval() {
1132+
var list = new ElementAwareArrayList<String>();
1133+
list.add("a");
1134+
list.add("b");
1135+
list.add("c");
1136+
list.add("d");
1137+
1138+
var it = list.listIterator();
1139+
it.next(); // a
1140+
it.next(); // b
1141+
it.remove(); // gap at physical 1; logical [a, c, d]
1142+
it.next(); // c
1143+
it.next(); // d -> logical end, interior gap still present
1144+
it.add("e"); // append via addEntry while gap exists
1145+
1146+
assertThat(it.hasNext()).isFalse();
1147+
assertThat(it.previous()).isEqualTo("e");
1148+
assertThat(it.previous()).isEqualTo("d");
1149+
assertThat(it.previous()).isEqualTo("c");
1150+
assertThat(list).containsExactly("a", "c", "d", "e");
1151+
}
1152+
11981153
@Test
11991154
@DisplayName("next() past end throws NoSuchElementException")
12001155
void noSuchElement() {
@@ -1234,67 +1189,6 @@ void startAtIndex() {
12341189
assertThat(it.previous()).isEqualTo("b");
12351190
}
12361191

1237-
@Test
1238-
@DisplayName("listIterator(index) on gappy list compacts before starting")
1239-
void startAtIndexWithGaps() {
1240-
var list = new ElementAwareArrayList<String>();
1241-
list.add("a");
1242-
var entry = list.addEntry("b");
1243-
list.add("c");
1244-
list.add("d");
1245-
entry.remove();
1246-
1247-
var it = list.listIterator(1);
1248-
assertThat(it.next()).isEqualTo("c");
1249-
assertThat(it.previous()).isEqualTo("c");
1250-
assertThat(it.previous()).isEqualTo("a");
1251-
}
1252-
1253-
@Test
1254-
@DisplayName("forward iteration through gaps then full backward traversal")
1255-
void forwardWithGapsThenBackward() {
1256-
var list = new ElementAwareArrayList<String>();
1257-
list.add("a");
1258-
var entryB = list.addEntry("b");
1259-
list.add("c");
1260-
var entryD = list.addEntry("d");
1261-
list.add("e");
1262-
entryB.remove();
1263-
entryD.remove();
1264-
// Physical: [a, null, c, null, e]; gaps never compacted during forward scan.
1265-
1266-
var it = list.listIterator();
1267-
assertThat(it.next()).isEqualTo("a");
1268-
assertThat(it.next()).isEqualTo("c");
1269-
assertThat(it.next()).isEqualTo("e");
1270-
assertThat(it.hasNext()).isFalse();
1271-
1272-
// previous() must traverse back through the uncompacted gaps.
1273-
assertThat(it.previous()).isEqualTo("e");
1274-
assertThat(it.previous()).isEqualTo("c");
1275-
assertThat(it.previous()).isEqualTo("a");
1276-
assertThat(it.hasPrevious()).isFalse();
1277-
}
1278-
1279-
@Test
1280-
@DisplayName("backward iteration from end of gappy list (gaps at head and middle)")
1281-
void backwardFromEndWithGaps() {
1282-
var list = new ElementAwareArrayList<String>();
1283-
var entryA = list.addEntry("a");
1284-
list.add("b");
1285-
var entryC = list.addEntry("c");
1286-
list.add("d");
1287-
entryA.remove();
1288-
entryC.remove();
1289-
// Physical: [null, b, null, d], logical: [b, d].
1290-
1291-
var it = list.listIterator(list.size());
1292-
assertThat(it.hasPrevious()).isTrue();
1293-
assertThat(it.previous()).isEqualTo("d");
1294-
assertThat(it.previous()).isEqualTo("b");
1295-
assertThat(it.hasPrevious()).isFalse();
1296-
}
1297-
12981192
@Test
12991193
@DisplayName("iterator remove on list that already has gaps")
13001194
void removeViaIteratorOnGappyList() {
@@ -1304,7 +1198,7 @@ void removeViaIteratorOnGappyList() {
13041198
list.add("c");
13051199
list.add("d");
13061200
entryB.remove();
1307-
// Physical: [a, null, c, d], logical: [a, c, d].
1201+
// listIterator() compacts the b-gap away first; the gap under test is the one it.remove() creates mid-iteration.
13081202

13091203
var it = list.listIterator();
13101204
assertThat(it.next()).isEqualTo("a");
@@ -1328,7 +1222,7 @@ void nextRemoveNextPreviousCycle() {
13281222
list.add("b");
13291223
list.add("c");
13301224
entryX.remove();
1331-
// Physical: [a, null, b, c], logical: [a, b, c].
1225+
// listIterator() compacts the x-gap away first; the gap under test is the one it.remove() creates mid-iteration.
13321226

13331227
var it = list.listIterator();
13341228
assertThat(it.next()).isEqualTo("a");

0 commit comments

Comments
 (0)