Skip to content

Commit a452a16

Browse files
committed
Add org.cactoos.map.Immutable decorator and related docs/tests
Introduce a read-only Map decorator consistent with list/set Immutable classes, document usage in the README (including SplitPreserveAllTokens examples for the existing text API), and add unit tests for map.Immutable and TriFuncSplitPreserve. Keep the Maven CI matrix on ubuntu-24.04, windows-2022, and macos-15. Pass -Dhone.skipWithoutDocker=true so hone runs when Docker is present (Ubuntu) and is skipped cleanly on runners without Docker. Closes #1835
1 parent d390ba5 commit a452a16

5 files changed

Lines changed: 954 additions & 21 deletions

File tree

.github/workflows/mvn.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ jobs:
1515
timeout-minutes: 15
1616
runs-on: ${{ matrix.os }}
1717
strategy:
18+
fail-fast: false
1819
matrix:
1920
os: [ubuntu-24.04, windows-2022, macos-15]
2021
java: [23]
@@ -32,4 +33,5 @@ jobs:
3233
${{ runner.os }}-jdk-${{ matrix.java }}-maven-
3334
- run: java -version
3435
- run: mvn -version
35-
- run: mvn --errors --batch-mode clean install -Pqulice
36+
# hone-maven-plugin needs Docker; skip it when Docker is unavailable (macOS/Windows runners).
37+
- run: mvn --errors --batch-mode clean install -Pqulice -Dhone.skipWithoutDocker=true

README.md

Lines changed: 127 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,63 @@ new Upper(
110110
);
111111
```
112112

113+
### Splitting Text
114+
115+
To split text using Java's standard behavior:
116+
117+
```java
118+
Iterable<Text> parts = new Split("hello,world", ",");
119+
// Result: ["hello", "world"]
120+
```
121+
122+
### Splitting with Preserved Empty Tokens
123+
124+
Java's `String.split()` discards trailing empty tokens.
125+
Use `SplitPreserveAllTokens` when you need to preserve ALL tokens,
126+
including empty ones created by adjacent or trailing delimiters:
127+
128+
```java
129+
// Standard split loses trailing empty token
130+
"a,b,".split(",") // Returns: ["a", "b"] - trailing empty LOST!
131+
132+
// SplitPreserveAllTokens preserves it
133+
Iterable<Text> parts = new SplitPreserveAllTokens("a,b,", ",");
134+
// Result: ["a", "b", ""] - all tokens preserved!
135+
```
136+
137+
This is essential for parsing CSV/TSV data where empty fields are meaningful:
138+
139+
```java
140+
// Parse CSV with empty fields
141+
Iterable<Text> fields = new SplitPreserveAllTokens(
142+
"John,,Smith,,"
143+
","
144+
);
145+
// Result: ["John", "", "Smith", "", ""] - all 5 fields!
146+
147+
// With spaces as delimiter
148+
Iterable<Text> words = new SplitPreserveAllTokens(" hello world ");
149+
// Result: ["", "hello", "", "world", ""]
150+
151+
// Default delimiter is space
152+
Iterable<Text> tokens = new SplitPreserveAllTokens("a b c");
153+
// Result: ["a", "b", "c"]
154+
155+
// With limit on number of tokens
156+
Iterable<Text> limited = new SplitPreserveAllTokens("a,b,c,d", ",", 2);
157+
// Result: ["a", "b"]
158+
```
159+
160+
**Key guarantee**: With N delimiters, you always get exactly N+1 tokens.
161+
162+
| Input | Delimiter | `String.split()` | `SplitPreserveAllTokens` |
163+
| ------- | ----------- | ------------------ | -------------------------- |
164+
| `"a,b,"` | `,` | `["a", "b"]` | `["a", "b", ""]` |
165+
| `",,"` | `,` | `[]` | `["", "", ""]` |
166+
| `","` | `,` | `[]` | `["", ""]` |
167+
168+
> **Note**: The delimiter is matched as a literal string, not a regex.
169+
113170
## Iterables/Collections/Lists/Sets
114171

115172
More about it here:
@@ -254,6 +311,54 @@ final Set<String> sorted = new org.cactoos.set.Sorted<>(
254311
);
255312
```
256313

314+
## Maps
315+
316+
To create a simple map:
317+
318+
```java
319+
Map<String, Integer> map = new MapOf<>(
320+
new MapEntry<>("one", 1),
321+
new MapEntry<>("two", 2),
322+
new MapEntry<>("three", 3)
323+
);
324+
```
325+
326+
### Immutable Maps
327+
328+
To create an immutable (read-only) map that prevents any modifications:
329+
330+
```java
331+
Map<String, Integer> map = new org.cactoos.map.Immutable<>(
332+
new MapOf<>(
333+
new MapEntry<>("one", 1),
334+
new MapEntry<>("two", 2)
335+
)
336+
);
337+
map.get("one"); // returns 1
338+
map.put("three", 3); // throws UnsupportedOperationException!
339+
map.clear(); // throws UnsupportedOperationException!
340+
```
341+
342+
The `Immutable` map decorator guarantees that:
343+
344+
- Mutating methods (`put`, `remove`, `putAll`, `clear`) are blocked
345+
- Views from `keySet()`, `values()`, and `entrySet()` are immutable
346+
- `Entry.setValue()` is blocked on entries from `entrySet()`
347+
348+
This is useful when you need to pass a map to untrusted code or ensure
349+
a map cannot be accidentally modified:
350+
351+
```java
352+
// Safe to pass to any method - cannot be modified
353+
public Map<String, Config> getConfiguration() {
354+
return new Immutable<>(this.config);
355+
}
356+
```
357+
358+
> **Note**: This is a decorator, not a copy. If the underlying map is modified
359+
> through another reference, changes will be visible. For a true snapshot,
360+
> copy the data first.
361+
257362
## Funcs and Procs
258363

259364
This is a traditional `foreach` loop:
@@ -331,6 +436,7 @@ final String text = new TextOfDateTime(date).asString();
331436
| `And` | `Iterables.all()` | - | - |
332437
| `Filtered` | `Iterables.filter()` | ? | - |
333438
| `FormattedText` | - | - | `String.format()` |
439+
| `map.Immutable` | `ImmutableMap` | `UnmodifiableMap` | `unmodifiableMap()` |
334440
| `IsBlank` | - | `StringUtils.isBlank()` | - |
335441
| `Joined` | - | - | `String.join()` |
336442
| `LengthOf` | - | - | `String#length()` |
@@ -342,6 +448,7 @@ final String text = new TextOfDateTime(date).asString();
342448
| `Reversed` | - | - | `StringBuilder#reverse()` |
343449
| `Rotated` | - | `StringUtils.rotate()` | - |
344450
| `Split` | - | - | `String#split()` |
451+
| `SplitPreserveAllTokens` | - | `StringUtils.splitPreserveAllTokens()` | - |
345452
| `StickyList` | `Lists.newArrayList()` | ? | `Arrays.asList()` |
346453
| `Sub` | - | - | `String#substring()` |
347454
| `SwappedCase` | - | `StringUtils.swapCase()` | - |
@@ -449,26 +556,26 @@ in GitHub precommits.
449556

450557
## Contributors
451558

452-
* [@yegor256](https://github.com/yegor256) as Yegor Bugayenko ([Blog](http://www.yegor256.com))
453-
* [@g4s8](https://github.com/g4s8) as [Kirill Che.](mailto:g4s8.public@gmail.com)
454-
* [@fabriciofx](https://github.com/fabriciofx) as Fabrício Cabral
455-
* [@englishman](https://github.com/englishman) as Andriy Kryvtsun
456-
* [@VsSekorin](https://github.com/VsSekorin) as Vseslav Sekorin
457-
* [@DronMDF](https://github.com/DronMDF) as Andrey Valyaev
458-
* [@dusan-rychnovsky](https://github.com/dusan-rychnovsky) as Dušan Rychnovský ([Blog](http://blog.dusanrychnovsky.cz/))
459-
* [@timmeey](https://github.com/timmeey) as Tim Hinkes ([Blog](https://blog.timmeey.de))
460-
* [@alex-semenyuk](https://github.com/alex-semenyuk) as Alexey Semenyuk
461-
* [@smallcreep](https://github.com/smallcreep) as Ilia Rogozhin
462-
* [@memoyil](https://github.com/memoyil) as Mehmet Yildirim
463-
* [@llorllale](https://github.com/llorllale) as George Aristy
464-
* [@driver733](https://github.com/driver733) as Mikhail Yakushin
465-
* [@izrik](https://github.com/izrik) as Richard Sartor
466-
* [@Vatavuk](https://github.com/Vatavuk) as Vedran Grgo Vatavuk
467-
* [@dgroup](https://github.com/dgroup) as Yurii Dubinka
468-
* [@iakunin](https://github.com/iakunin) as Maksim Iakunin
469-
* [@fanifieiev](https://github.com/fanifieiev) as Fevzi Anifieiev
470-
* [@victornoel](https://github.com/victornoel) as Victor Noël
471-
* [@paulodamaso](https://github.com/paulodamaso) as Paulo Lobo
559+
- [@yegor256](https://github.com/yegor256) as Yegor Bugayenko ([Blog](http://www.yegor256.com))
560+
- [@g4s8](https://github.com/g4s8) as [Kirill Che.](mailto:g4s8.public@gmail.com)
561+
- [@fabriciofx](https://github.com/fabriciofx) as Fabrício Cabral
562+
- [@englishman](https://github.com/englishman) as Andriy Kryvtsun
563+
- [@VsSekorin](https://github.com/VsSekorin) as Vseslav Sekorin
564+
- [@DronMDF](https://github.com/DronMDF) as Andrey Valyaev
565+
- [@dusan-rychnovsky](https://github.com/dusan-rychnovsky) as Dušan Rychnovský ([Blog](http://blog.dusanrychnovsky.cz/))
566+
- [@timmeey](https://github.com/timmeey) as Tim Hinkes ([Blog](https://blog.timmeey.de))
567+
- [@alex-semenyuk](https://github.com/alex-semenyuk) as Alexey Semenyuk
568+
- [@smallcreep](https://github.com/smallcreep) as Ilia Rogozhin
569+
- [@memoyil](https://github.com/memoyil) as Mehmet Yildirim
570+
- [@llorllale](https://github.com/llorllale) as George Aristy
571+
- [@driver733](https://github.com/driver733) as Mikhail Yakushin
572+
- [@izrik](https://github.com/izrik) as Richard Sartor
573+
- [@Vatavuk](https://github.com/Vatavuk) as Vedran Grgo Vatavuk
574+
- [@dgroup](https://github.com/dgroup) as Yurii Dubinka
575+
- [@iakunin](https://github.com/iakunin) as Maksim Iakunin
576+
- [@fanifieiev](https://github.com/fanifieiev) as Fevzi Anifieiev
577+
- [@victornoel](https://github.com/victornoel) as Victor Noël
578+
- [@paulodamaso](https://github.com/paulodamaso) as Paulo Lobo
472579

473580
[blog]: http://www.yegor256.com/2017/06/22/object-oriented-input-output-in-cactoos.html
474581
[OffsetDateTime]: https://docs.oracle.com/javase/8/docs/api/java/time/OffsetDateTime.html

0 commit comments

Comments
 (0)