General FizzBuzz/jFizzBuzz

From Rosetta Code
/**
 * @file GeneralFizzBuzz.java
 */

import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Scanner;


/**
 * This class follows the "God class" pattern.
 * See {@link #main(String[])} for example usage.
 */
class FizzBuzzer
implements
    Iterable<FizzBuzzer.Entry>,
    Iterator<FizzBuzzer.Entry>
{
    private static final long DEFAULT_START_NUM = 1;
    private static final long DEFAULT_MAX_NUM = Long.MAX_VALUE;

    public class Entry {

        public final long num;
        public final List<String> names;

        private Entry(Long num, List<String> names) {
            this.num = num;
            this.names = names;
        }
    }

    private Map<Integer, String> modToName = new HashMap<>();

    private long currNum = DEFAULT_START_NUM;
    private long maxNum  = DEFAULT_MAX_NUM;
    private Map<Long, List<Integer>> schedule = new HashMap<>();

    public long getCurrNum() { return currNum; }
    public void setCurrNum(long currNum) { this.currNum = currNum; }

    public long getMaxNum() { return maxNum; }
    public void setMaxNum(long maxNum) { this.maxNum = maxNum; }


    private List<Integer> scheduledAt(long num) {
        return schedule.computeIfAbsent(num, num_ -> new ArrayList<>());
    }

    private void scheduleNearest(Integer mod) {

        long nearestNum = currNum + (mod - currNum % mod);

        scheduledAt(nearestNum).add(mod);
    }

    public boolean add(Integer mod, String name) {

        if (modToName.containsKey(mod)) {
            return false;
        }

        modToName.put(mod, name);
        scheduleNearest(mod);

        return true;
    }

    public void addAll(Map<Integer, String> modToName) {

        for (Map.Entry<Integer, String> modAndName : modToName.entrySet()) {
            add(modAndName.getKey(), modAndName.getValue());
        }
    }


    @Override
    public Entry next() {

        List<Integer> currMods = scheduledAt(currNum);

        List<String> currNames = new ArrayList<>();

        for (Integer m : currMods) {

            String name = modToName.get(m);
            currNames.add(name);

            long newNum = currNum + m;
            scheduledAt(newNum).add(m);
        }

        Entry result = new Entry(currNum, currNames);

        currNum++;

        return result;
    }

    @Override
    public void remove() {
        schedule.remove(currNum - 1);
    }

    @Override
    public boolean hasNext() {
        return (maxNum < 0) || (currNum <= maxNum);
    }

    @Override
    public Iterator<Entry> iterator() {
        return this;
    }


    public static FizzBuzzer readFrom(InputStream in) {

        FizzBuzzer fizzBuzzer = new FizzBuzzer();

        try (Scanner scanner = new Scanner(in)) {

            long maxNum = scanner.nextLong();
            fizzBuzzer.setMaxNum(maxNum);

            while (scanner.hasNext()) {

                Integer mod = scanner.nextInt();
                scanner.skip("[\t ]*");
                String name = scanner.nextLine();

                fizzBuzzer.add(mod, name);
            }
        }

        return fizzBuzzer;
    }

    public void printTo(OutputStream out) {

        try (PrintWriter writer = new PrintWriter(out)) {

            for (FizzBuzzer.Entry e : this) {

                String strNames = String.join(" ", e.names);
                String line = String.format("%d %s", e.num, strNames);

                writer.println(line);
                writer.flush();

                remove();
            }
        }
    }

    @Override
    public String toString() {
        return String.format("%s; current %d; max %d",
                             modToName, currNum, maxNum);
    }
}

class GeneralFizzBuzz {

    public static void main(String[] args) {

        try {
            FizzBuzzer.readFrom(System.in).printTo(System.out);
        }

        catch (NoSuchElementException | IllegalStateException e) {

            System.err.format("Error: %s\n", e.toString());
            System.exit(1);
        }
    }
}