diff --git a/pom.xml b/pom.xml index 9ff44fd..d97c8ab 100644 --- a/pom.xml +++ b/pom.xml @@ -13,8 +13,8 @@ org.apache.maven.plugins maven-compiler-plugin - 8 - 8 + 9 + 9 diff --git a/src/main/java/net/abhinavsarkar/former/StringDecompression.java b/src/main/java/net/abhinavsarkar/former/StringDecompression.java new file mode 100644 index 0000000..b201d21 --- /dev/null +++ b/src/main/java/net/abhinavsarkar/former/StringDecompression.java @@ -0,0 +1,233 @@ +package net.abhinavsarkar.former; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * https://techdevguide.withgoogle.com/resources/compress-decompression/ + */ +public class StringDecompression { + public static void main(String[] args) { + System.out.println(decompress("10[a]")); + System.out.println(decompress("3[abc]4[ab]c")); + System.out.println(decompress("2[3[a]b]")); + System.out.println(decompress("2[]b")); + System.out.println(decompress("0[abc]v")); + System.out.println(decompress("szs")); + } + + static String decompress(String s) { + return parse(s).toString(); + } + + private static CompressedString parse(String s) { + Optional> parsed = CompressedString.PARSER.parse(s); + if (!parsed.isPresent()) { + throw new IllegalArgumentException("Unable to decompress"); + } + return parsed.get().second; + } +} + +class Tuple { + A first; + B second; + + Tuple(A first, B second) { + this.first = first; + this.second = second; + } + + Tuple map(Function f) { + return new Tuple<>(first, f.apply(second)); + } +} + +@FunctionalInterface +interface Parser { + Optional> parse(I input); + + default

Parser map(Function mapper) { + return input -> this.parse(input).map(tup -> tup.map(mapper)); + } + + default

Parser seqApply(Parser> parser) { + return input -> parser.parse(input) + .flatMap(ftup -> this.parse(ftup.first).map(tup -> tup.map(ftup.second))); + } + + default

Parser leftSeqApply(Parser parser) { + return this.apply((x, y) -> y, parser); + } + + default

Parser rightSeqApply(Parser parser) { + return this.apply((x, y) -> x, parser); + } + + default Parser apply(BiFunction function, Parser parser) { + return parser.seqApply(this.map(o -> p -> function.apply(o, p))); + } + + default Parser or(Parser parser) { + return input -> this.parse(input).or(() -> parser.parse(input)); + } + + default Parser> many() { + return input -> { + I rest = input; + List output = new ArrayList<>(); + + Optional> parsed = this.parse(rest); + while (parsed.isPresent()) { + Tuple tup = parsed.get(); + rest = tup.first; + output.add(tup.second); + parsed = this.parse(rest); + } + return Optional.of(new Tuple<>(rest, output)); + }; + } + + default Parser> some() { + return input -> { + Tuple> output = many().parse(input).get(); + if (output.second.isEmpty()) { + return Optional.empty(); + } else { + return Optional.of(output); + } + }; + } + +} + +class CharPredicateParser implements Parser { + private Predicate predicate; + + CharPredicateParser(Predicate predicate) { + this.predicate = predicate; + } + + @Override + public Optional> parse(String input) { + if (input.isEmpty()) { + return Optional.empty(); + } + + Character i = input.charAt(0); + if (predicate.test(i)) { + return Optional.of(new Tuple<>(input.substring(1), i)); + } + + return Optional.empty(); + } +} + +class Parsers { + public static final Parser DIGIT_PARSER = + new CharPredicateParser(Character::isDigit); + + public static final Parser NUMBER_PARSER = + DIGIT_PARSER + .some() + .map(Parsers::digitsToInt); + + public static final Parser ENGLISH_LOWER_CASE_CHAR_PARSER = + new CharPredicateParser(c -> c >= 97 && c <= 122); + + public static Parser charParser(Character c) { + return new CharPredicateParser(c::equals); + } + + private static Integer digitsToInt(List digits) { + int i = 0; + for (char c : digits) { + i = i * 10 + (c - 48); + } + return i; + } +} + +class CompressedString { + private List parts; + + CompressedString(List parts) { + this.parts = parts; + } + + interface Part { + String toString(); + } + + @Override + public String toString() { + return parts.stream() + .map(Object::toString) + .collect(Collectors.joining()); + } + + public static final Parser PARSER = + CompressedPart.PARSER + .or(StringPart.PARSER) + .many() + .map(CompressedString::new); +} + + +class StringPart implements CompressedString.Part { + private String string; + + StringPart(String string) { + this.string = string; + } + + @Override + public String toString() { + return this.string; + } + + public static final Parser PARSER = + Parsers.ENGLISH_LOWER_CASE_CHAR_PARSER + .some() + .map(chars -> new StringPart(charListToString(chars))); + + private static String charListToString(List chars) { + return chars.stream() + .map(String::valueOf) + .collect(Collectors.joining()); + } +} + +class CompressedPart implements CompressedString.Part { + private int repetition; + private CompressedString string; + + CompressedPart(int repetition, CompressedString string) { + this.repetition = repetition; + this.string = string; + } + + @Override + public String toString() { + String s = string.toString(); + return Stream.generate(() -> s) + .limit(repetition) + .collect(Collectors.joining()); + } + + private static final Parser BRACKETED_STRING_PARSER = + Parsers.charParser('[') + .leftSeqApply(input -> CompressedString.PARSER + .rightSeqApply(Parsers.charParser(']')) + .parse(input)); + + public static final Parser PARSER = + Parsers.NUMBER_PARSER.apply(CompressedPart::new, BRACKETED_STRING_PARSER); +} +