12

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) {
    }
}
Alexis C.
  • 82,826
  • 18
  • 154
  • 166
elect
  • 5,677
  • 9
  • 44
  • 97
  • 2
    Your question is unclear - what are you trying to switch on? It sounds like you might want to *first* write a method which reads the lines and returns a `Map` (where `ConfigurationBlock` is your own type, or maybe use `Properties`). – Jon Skeet May 27 '16 at 09:02
  • 1
    Start by showing what you want to do with a normal switch statement without the Stream – Nicolas Filotto May 27 '16 at 09:07
  • I think stream is ideal when the order of inputs is insignificant. In your ini example, however, the order of lines is critical. Thus, the element of the top-level stream should not be lines of text, but config sections. And each section consists of a stream of order-insensitive lines of text. – Crend King May 27 '16 at 09:37
  • Unfortunately it looks so, @MaartenBodewes.. I was kind of hoping there was a few-lines-elegant method – elect May 27 '16 at 16:01

4 Answers4

11

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));
});
Holger
  • 243,335
  • 30
  • 362
  • 661
7

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.

Hank D
  • 5,654
  • 2
  • 20
  • 31
5

Attempt:

try {
        Path file = Paths.get("G:\\tmp", "img.ini");
        Stream<String> lines = Files.lines(file);

        lines.filter(line->{
            if("pos".equalsIgnoreCase(line.split("=")[0])){
                //process pos line here
                System.out.println("pos"+line);
                return false;
            }
            return true;
        }).filter(line->{
            System.out.println("2"+line);
            if("Collapsed".equalsIgnoreCase(line.split("=")[0])){
                //process Collapsed line here
                System.out.println("Collapsed"+line);
                return false;
            }
            return true;
        }).filter(line->{
            System.out.println("3"+line);
            if("Size".equalsIgnoreCase(line.split("=")[0])){
                //process Size line here
                System.out.println("Size"+line);
                return false;
            }
            return true;
        }).forEach(line->{
            //settings = new Settings();
        });;
    } catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
Vasco
  • 734
  • 1
  • 4
  • 19
1

Another way to read your config file:

public class Main {
    public static void main(String[] args) throws IOException {
        Path path = Paths.get("D:\\Development\\workspace\\Application\\src\\main\\resources\\init.txt");
        String content = new String(Files.readAllBytes(path));

        Map<String, Config> configMap = Stream.of(content.split("\\n\\r"))
            .map(config -> Arrays.asList(config.split("\\r")))
            .collect(HashMap<String, Config>::new, (map, list) -> {
                String header = list.get(0);
                String pos = list.get(1);
                String size = list.get(2);
                String collapsed = list.get(3);
                map.put(header, new Config(pos.substring(pos.indexOf("=") + 1), size.substring(size.indexOf("=") + 1), collapsed.substring(collapsed.indexOf("=") + 1)));
            }, (m, u) -> {});

        System.out.println(configMap);
    }
}

class Config {
    public String pos;
    public String size;
    public String collapsed;

    public Config(String pos, String size, String collapsed) {
        this.pos = pos;
        this.size = size;
        this.collapsed = collapsed;
    }

    @Override
    public String toString() {
        return "Config{" +  "pos='" + pos + '\'' + ", size='" + size + '\'' + 
               ", collapsed='" + collapsed + '\'' + '}';
    }
}

Result will be map:

{
    [Debug]=Config{pos='7,79', size='507,392', collapsed='0'}, 
    [ImGui Demo]=Config{pos='320,5', size='550,680', collapsed='0'}
}
Alexander Ivanov
  • 2,143
  • 1
  • 11
  • 15
  • Why would you hardcode the line endings? Just to ensure it won't work on a Mac or Linux? And why use backslashes instead of forward slashes in the path? Just so you get to escape them? – David Conrad May 27 '16 at 12:01