I have a text file imgui.ini
containing:
[Debug]
Pos=7,79
Size=507,392
Collapsed=0
[ImGui Demo]
Pos=320,5
Size=550,680
Collapsed=0
For each "element" I always have Pos
, Size
and Collapsed
and I need to read them.
I would like to use, if possible, java 8 streams.
Is it possible to simulate a switch statement behaviour?
try (Stream<String> stream = Files.lines(Paths.get(context.io.iniFilename))) {
...
/*
switch(string) {
case "Pos":
settings.pos = value;
break;
case "Size":
settings.size = value;
break;
case "Collapsed":
settings.collapsed = value;
break;
}
*/
} catch (IOException e) {
}
}
Introduced in Java 8, the Stream API is used to process collections of objects. A stream is a sequence of objects that supports various methods which can be pipelined to produce the desired result. A stream is not a data structure instead it takes input from the Collections, Arrays or I/O channels.
Java Streams are basically a pipeline of aggregate operations that can be applied to process a sequence of elements. An aggregate operation is a higher-order function that receives a behaviour in a form of a function or lambda, and that behaviour is what gets applied to our sequence.
What are the two types of Streams offered by java 8? Explanation: Sequential stream and parallel stream are two types of stream provided by java.
The best way to parse such a file (without using dedicated 3rd party libraries), is via the regex API, and its front-end class Scanner
. Unfortunately, the best operations to implement it via Stream API, are currently missing. Namely, Matcher.results()
and Scanner.findAll(…)
are not there yet. So unless we want to wait until Java 9, we have to create similar methods for a Java 8 compatible solution:
public static Stream<MatchResult> findAll(Scanner s, Pattern pattern) {
return StreamSupport.stream(new Spliterators.AbstractSpliterator<MatchResult>(
1000, Spliterator.ORDERED|Spliterator.NONNULL) {
public boolean tryAdvance(Consumer<? super MatchResult> action) {
if(s.findWithinHorizon(pattern, 0)!=null) {
action.accept(s.match());
return true;
}
else return false;
}
}, false);
}
public static Stream<MatchResult> results(Matcher m) {
return StreamSupport.stream(new Spliterators.AbstractSpliterator<MatchResult>(
m.regionEnd()-m.regionStart(), Spliterator.ORDERED|Spliterator.NONNULL) {
public boolean tryAdvance(Consumer<? super MatchResult> action) {
if(m.find()) {
action.accept(m.toMatchResult());
return true;
}
else return false;
}
}, false);
}
Using methods with a similar semantic allows us to replace their usage with the standard API methods, once Java 9 is released and becomes commonplace.
Using these two operations, you can parse your file using
Pattern groupPattern=Pattern.compile("\\[(.*?)\\]([^\\[]*)");
Pattern attrPattern=Pattern.compile("(.*?)=(.*)\\v");
Map<String, Map<String, String>> m;
try(Scanner s=new Scanner(Paths.get(context.io.iniFilename))) {
m = findAll(s, groupPattern).collect(Collectors.toMap(
gm -> gm.group(1),
gm -> results(attrPattern.matcher(gm.group(2)))
.collect(Collectors.toMap(am->am.group(1), am->am.group(2)))));
}
the resulting map m
holds all information, mapping from the group names to another map holding the key/value pairs, i.e. you can print an equivalent .ini
file using:
m.forEach((group,attr)-> {
System.out.println("["+group+"]");
attr.forEach((key,value)->System.out.println(key+"="+value));
});
Focusing on the question "is there a way to simulate switch statement behavior", I think the answer is that you could, with a little effort. I asked myself that a couple of years ago, and did the following as an exercise (and then never used it again):
private static <T> Predicate<T> testAndConsume(Predicate<T> pred, Consumer<T> cons) {
return t -> {
boolean result = pred.test(t);
if (result) cons.accept(t);
return result;
};
}
public static class SwitchConsumer<T> {
Predicate<T> conditionalConsumer;
private SwitchConsumer(Predicate<T> pred) {
conditionalConsumer = pred;
}
public static <C> SwitchConsumer<C> inCase(Predicate<C> pred, Consumer<C> cons) {
return new SwitchConsumer<>(testAndConsume(pred, cons));
}
public SwitchConsumer<T> elseIf(Predicate<T> pred, Consumer<T> cons) {
return new SwitchConsumer<>(conditionalConsumer.or(testAndConsume(pred,cons)));
}
public Consumer<T> elseDefault(Consumer<T> cons) {
return testAndConsume(conditionalConsumer.negate(),cons)::test; // ::test converts Predicate to Consumer
}
}
testAndConsume
composes a Predicate
and a Consumer
, creating a Predicate
that returns the same value but calls the Consumer
as a side-effect if the value is true. That becomes the basis for each "case" in the "switch". Each "case" is strung together by Predicate.or()
, which provides the short-circuiting "else-if" nature of the switch. Finally, the composed Predicate
is turned into a Consumer
by adding ::test
to the Predicate
.
Applying it to your code snippet, it looks like this:
Stream.of("Pos=320,5", "Size=550,680", "Collapsed=0")
.map(s -> s.split("="))
.forEach(SwitchConsumer.<String[]>
inCase(arr -> "Pos".equals(arr[0]), arr -> settings.pos = arr[1])
.elseIf(arr -> "Size".equals(arr[0]), arr -> settings.size = arr[1])
.elseIf(arr -> "Collapsed".equals(arr[0]), arr -> settings.collapsed = arr[1])
.elseDefault(arr -> {}));
That's about as switch-ish as it can get without an actual switch in a Consumer
body.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With