/*
* Kodkod -- Copyright (c) 2005-present, Emina Torlak
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package kodkod.examples.sudoku;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Random;
import kodkod.instance.TupleSet;
import kodkod.instance.Universe;
import kodkod.util.ints.IntBitSet;
import kodkod.util.ints.IntIterator;
import kodkod.util.ints.IntSet;
/**
* A class for managing NxN (where N is a perfect square) Sudoku puzzles loaded from a file.
* @specfield N, size : int
* @specfield puzzles: [0..size) ->one TupleSet
* @invariant some r: int | r*r = N
* @invariant all p1, p2: puzzles | p1.universe = p2.universe
* @invariant all p: puzzles[int] | p.arity = 3 and p.universe = { i: Integer | 1 <= i.intValue() <= N }
* @author Emina Torlak
*/
public final class SudokuDatabase implements Iterable<TupleSet>{
private final TupleSet[] puzzles;
private SudokuDatabase(String[] puzzles) {
this.puzzles = new TupleSet[puzzles.length];
if (puzzles.length>0) {
this.puzzles[0] = SudokuParser.parse(puzzles[0]);
final Universe univ = this.puzzles[0].universe();
for(int i = 1; i < puzzles.length; i++) {
this.puzzles[i] = SudokuParser.parse(puzzles[i], univ);
}
}
}
/**
* Constructs a new sudoku database out of the given collection of puzzles.
* Each tupleset in the given collection is assumed to be drawn from the same
* universe, to have arity 3, and to have all of its tuples drawn from the subset
* {1..N} of the given universe where N is a perfect square.
* @requires all p1, p2: puzzles | p1.universe = p2.universe
* @requires some r: int | all p: puzzles | p.arity = 3 and p.universe = { i: Integer | 1 <= i.intValue() <= r*r }
* @ensures this.puzzles' = puzzles.toArray()
*/
private SudokuDatabase(Collection<TupleSet> puzzles) {
this.puzzles = puzzles.toArray(new TupleSet[puzzles.size()]);
}
/**
* Returns the size of this database.
* @return #this.puzzles
*/
public int size() { return puzzles.length; }
/**
* Returns the clues for ith puzzle.
* @requires 0 <= i < this.size()
* @return this.puzzles[i]
*/
public TupleSet puzzle(int i) { return this.puzzles[i].clone(); }
/**
* Returns an iterator over the puzzles in this database. The returned
* iterator does not support removal.
* @return iterator over the puzzles in this database.
*/
public Iterator<TupleSet> iterator() {
return new Iterator<TupleSet>() {
private int i = 0;
public boolean hasNext() { return i < puzzles.length; }
public TupleSet next() {
if (!hasNext()) throw new NoSuchElementException();
return puzzle(i++);
}
public void remove() { throw new UnsupportedOperationException(); }
};
}
/**
* Writes out this database to the given file. Each puzzle is written out
* as a string of N*N numbers, where each subsequence of N numbers represents one row
* of the grid and zeros stand for blanks.
* @throws IOException
*/
public void write(String file) throws IOException {
write(new BufferedOutputStream(new FileOutputStream(file)));
}
/**
* Writes out this database to the stream. Each puzzle is written out
* as a string of N*N numbers, where each subsequence of N numbers represents one row
* of the grid and zeros stand for blanks.
* @throws IOException
*/
public void write(OutputStream stream) throws IOException {
if (puzzles.length==0) return;
final PrintWriter writer = new PrintWriter(stream);
for(TupleSet puzzle : puzzles) {
writer.println(SudokuParser.toString(puzzle));
}
writer.close();
}
/**
* Loads the puzzles from the given file into a fresh database.
* This method assumes that each line of the file represents
* an individual puzzle. The puzzle should be given as a string of
* N*N numbers, where each subsequence of N numbers represents one row
* of the grid and zeros stand for blanks. If N > 9, the numbers should
* be separated by spaces or tabs. If N <= 9, the spaces may be omitted.
* @requires the file is formatted as described above
* @return a sudoku database with the puzzles from the given file
* @throws IOException
*/
public static SudokuDatabase load(String file) throws IOException {
final List<TupleSet> puzzles = new ArrayList<TupleSet>(100);
final BufferedReader reader = new BufferedReader(new FileReader(file));
String puzzle = reader.readLine();
if (puzzle!=null) {
puzzles.add(SudokuParser.parse(puzzle));
final Universe univ = puzzles.get(0).universe();
while((puzzle=reader.readLine())!=null) {
puzzles.add(SudokuParser.parse(puzzle, univ));
}
}
reader.close();
return new SudokuDatabase(puzzles);
}
/**
* Extract the puzzles with the specified indices from the given file and
* returns them in a database. This method assumes that each line of the file represents
* an individual puzzle. The puzzle should be given as a string of
* N*N numbers, where each subsequence of N numbers represents one row
* of the grid and zeros stand for blanks. If N > 9, the numbers should
* be separated by spaces or tabs. If N <= 9, the spaces may be omitted.
* @requires the file is formatted as described above
* @requires the indices of puzzles requested are non-negative and do not exceed the number of puzzles in the file
* @return a sudoku database with the specified puzzles extracted from the given file
* @throws IOException
*/
@SuppressWarnings("unchecked")
public static SudokuDatabase load(String file, IntSet indices) throws IOException {
if (indices.isEmpty()) return new SudokuDatabase(Collections.EMPTY_LIST);
if (indices.min()<0) throw new IllegalArgumentException("Negative indices not permitted: " + indices.min());
final int numPuzzles = indices.size();
final String[] puzzles = new String[numPuzzles];
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
final IntIterator randItr = indices.iterator();
for(int i=0, last = -1; i<numPuzzles; i++) {
final int next = randItr.next();
for(int j = last+1; j<next; j++) {
reader.readLine(); // discard
}
last = next;
puzzles[i] = reader.readLine();
if (puzzles[i]==null) {
throw new IllegalArgumentException("The file " + file + " contains fewer than " + next + " puzzles.");
}
}
}
return new SudokuDatabase(puzzles);
}
/**
* Randomly selects the given number of puzzles from the specified
* file, and returns them in a database. The random number generator
* is initialized with the current time.
* This method assumes that each line of the file represents
* an individual puzzle. The puzzle should be given as a string of
* N*N numbers, where each subsequence of N numbers represents one row
* of the grid and zeros stand for blanks. If N > 9, the numbers should
* be separated by spaces or tabs. If N <= 9, the spaces may be omitted.
* @requires the file is formatted as described above
* @requires the number of puzzles requested does not exceed the number of puzzles in the file
* @return a sudoku database with the given number of puzzles, randomly
* selected from the specified file
* @throws IOException
*/
public static SudokuDatabase loadRandom(String file, int puzzles) throws IOException {
return loadRandom(file, puzzles, System.currentTimeMillis());
}
/**
* Randomly selects the given number of puzzles from the specified
* file, and returns them in a database. The random number generator
* is initialized with the given seed.
* This method assumes that each line of the file represents
* an individual puzzle. The puzzle should be given as a string of
* N*N numbers, where each row of N numbers represents one row
* of the grid and zeros stand for blanks. If N > 9, the numbers should
* be separated by spaces or tabs. If N <= 9, the spaces may be omitted.
* @requires the file is formatted as described above
* @requires the number of puzzles requested does not exceed the number of puzzles in the file
* @return a sudoku database with the given number of puzzles, randomly
* selected from the specified file using the specified seed.
* @throws IOException
*/
public static SudokuDatabase loadRandom(String file, int numPuzzles, long seed) throws IOException {
final Random rand = new Random(seed);
final IntSet rands = new IntBitSet(numPuzzles);
while(rands.size()<numPuzzles) {
rands.add(rand.nextInt(numPuzzles));
}
return load(file, rands);
}
private static void usage() {
System.out.println("Usage: java examples.sudoku.SudokuDatabase [-seed=<seed>] [-puzzles=<number of puzzles>] [-o=<output filename>] <database file>");
System.exit(1);
}
/**
* Usage: java examples.sudoku.SudokuDatabase [-seed=<seed>] [-puzzles=<number of puzzles>] [-o=<output filename>] <database file>
*/
public static void main(String[] args) {
if (args.length<1) usage();
final String file = args[args.length-1];
final Map<String,String> opts = SudokuParser.options(args);
try {
final SudokuDatabase db;
if (opts.containsKey("-puzzles")) {
final String numString = opts.get("-puzzles");
if (numString==null) usage();
final int numPuzzles = Integer.parseInt(numString);
if (opts.containsKey("-seed")) {
final String seedString = opts.get("-seed");
if (seedString==null) usage();
final long seed = Long.parseLong(seedString);
db = loadRandom(file, numPuzzles, seed);
} else {
db = loadRandom(file, numPuzzles);
}
} else {
db = load(file);
}
if (opts.containsKey("-o")) {
final String oString = opts.get("-o");
if (oString==null) usage();
db.write(oString);
} else {
db.write(System.out);
}
} catch (NumberFormatException nfe) {
usage();
} catch (IOException e) {
e.printStackTrace();
}
}
}