package org.opensha2.internal; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static org.opensha2.data.Data.checkWeights; import org.opensha2.data.Data; import org.opensha2.data.Indexing; import org.w3c.dom.Comment; import org.w3c.dom.Element; import org.xml.sax.Attributes; import com.google.common.base.CharMatcher; import com.google.common.base.Enums; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.base.Splitter; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableSortedMap; import com.google.common.collect.Iterables; import com.google.common.collect.Iterators; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.primitives.Doubles; import com.google.common.primitives.Ints; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import java.io.BufferedInputStream; import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; import java.util.AbstractCollection; import java.util.ArrayList; import java.util.Arrays; import java.util.BitSet; import java.util.Collection; import java.util.EnumMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.NavigableMap; /** * String, file, and XML parsing utilities. * * @author Peter Powers */ public final class Parsing { public static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); private static final Joiner.MapJoiner MAP_JOIN = Joiner.on(',').withKeyValueSeparator(":"); private static final Splitter.MapSplitter MAP_SPLIT = Splitter.on(',').trimResults() .withKeyValueSeparator(":"); private static final Splitter.MapSplitter MAP_MAP_SPLIT = Splitter.on(';').trimResults() .withKeyValueSeparator("::"); // TODO refactor as Converters /** * Convert a {@code Map<Enum, Double>} to a string with the form * {@code [ENUM_1 : 0.8, ENUM_2 : 0.2]}. * * @param map the {@code map} to convert * @return a string representation of the supplied {@code Map} */ public static <T extends Enum<T>> String enumValueMapToString(Map<T, Double> map) { Map<String, String> strMap = Maps.newHashMap(); for (Entry<T, Double> entry : map.entrySet()) { strMap.put(entry.getKey().name(), Double.toString(entry.getValue())); } return addBrackets(MAP_JOIN.join(strMap)); } /** * Convert a string of the form {@code [ENUM_1 : 0.8, ENUM_2 : 0.2]} to a * {@code Map<Enum, Double>}. The returned map is immutable. * * @param s the string to parse * @param type {@code Class} to use as the key of the returned map * @throws IllegalArgumentException if supplied string is malformed or empty, * or if weights do not sum to 1.0, within * {@link Data#WEIGHT_TOLERANCE} * @throws NumberFormatException if supplied string does not contain parseable * {@code double} values * @return a new immutable {@code Map<Enum, Double>} of identifiers and * weights */ public static <T extends Enum<T>> Map<T, Double> stringToEnumWeightMap(String s, Class<T> type) { Map<String, String> strMap = MAP_SPLIT.split(trimEnds(checkNotNull(s))); EnumMap<T, Double> wtMap = Maps.newEnumMap(type); Function<String, T> keyFunc = Enums.stringConverter(type); for (Entry<String, String> entry : strMap.entrySet()) { wtMap.put(keyFunc.apply(entry.getKey().trim()), Doubles.stringConverter().convert(entry.getValue())); } checkWeights(wtMap.values()); return wtMap; } /** * Convert a string of the form * {@code [6.5 :: [1.0 : 0.8, 5.0 : 0.2]; 10.0 :: [1.0 : 0.2, 5.0 : 0.8]]} to * a {@code NavigableMap<Double, Map<Double, Double>}. The returned map and * nested maps are immutable and should only be accessed via * {@code Map.entrySet()} references or using keys derived from * {@code Map.keySet()} as {@code Double} comparisons are inherently * problematic. * * @param s the string to parse * @throws IllegalArgumentException if {@code s} is malformed or empty, or if * weights do not sum to 1.0, within {@link Data#WEIGHT_TOLERANCE} * @throws NumberFormatException if {@code s} does not contain parseable * {@code double} values * @return a new immutable {@code Map<Double, Double>} of values and their * weights */ public static NavigableMap<Double, Map<Double, Double>> stringToValueValueWeightMap(String s) { Map<String, String> strMap = MAP_MAP_SPLIT.split(trimEnds(checkNotNull(s))); ImmutableSortedMap.Builder<Double, Map<Double, Double>> builder = ImmutableSortedMap .naturalOrder(); for (Entry<String, String> entry : strMap.entrySet()) { double key = Doubles.stringConverter().convert(entry.getKey()); builder.put(key, stringToValueWeightMap(entry.getValue())); } return builder.build(); } /** * Convert a string of the form {@code [1.0 : 0.4, 2.0 : 0.6]} to a * {@code NavigableMap<Double, Double>}. The returned map is immutable and * should only be accessed via {@code Map.entrySet()} references or using keys * derived from {@code Map.keySet()} as {@code Double} comparisons are * inherently problematic. * * @param s the string to parse * @throws IllegalArgumentException if {@code s} is malformed or if weights do * not sum to 1.0, within {@link Data#WEIGHT_TOLERANCE} * @throws NumberFormatException if {@code s} does not contain parseable * {@code double} values * @return a new immutable {@code Map<Double, Double>} of values and their * weights */ public static NavigableMap<Double, Double> stringToValueWeightMap(String s) { Map<String, String> strMap = MAP_SPLIT.split(trimEnds(checkNotNull(s))); ImmutableSortedMap.Builder<Double, Double> builder = ImmutableSortedMap.naturalOrder(); for (Entry<String, String> entry : strMap.entrySet()) { double key = Doubles.stringConverter().convert(entry.getKey()); double value = Doubles.stringConverter().convert(entry.getValue()); builder.put(key, value); } NavigableMap<Double, Double> valMap = builder.build(); checkWeights(valMap.values()); return valMap; } /** * Put SAX {@code Attributes} into a name-value {@code Map<String, String>} ( * {@code Attributes} are stateful when parsing XML). * * @param atts the {@code Attributes} to store in {@code Map} * @return a {@code Map} of name-value XML attribute pairs */ public static Map<String, String> toMap(Attributes atts) { Map<String, String> map = Maps.newHashMap(); for (int i = 0; i < checkNotNull(atts).getLength(); i++) { map.put(atts.getQName(i), atts.getValue(i)); } return map; } /** * Add an attribute with an {@code Enum.toString()} name and string value to * an XML {@link Element}. * * @param id the name identifier of the attribute * @param value the value of the attribute * @param parent the {@code Element} to add attribute to */ public static void addAttribute(Enum<?> id, String value, Element parent) { addAttribute(id.toString(), value, parent); } /** * Add an attribute with an {@code Enum.toString()} name and * {@code Enum.name()} value to an XML {@link Element}. * * @param id the name identifier of the attribute * @param value the value of the attribute * @param parent the {@code Element} to add attribute to */ public static void addAttribute(Enum<?> id, Enum<?> value, Element parent) { addAttribute(id.toString(), value.name(), parent); } /** * Add an attribute with an {@code Enum.toString()} name and * {@code Boolean.toString()} value to an XML {@link Element}. * * @param id the name identifier of the attribute * @param value the value of the attribute * @param parent the {@code Element} to add attribute to */ public static void addAttribute(Enum<?> id, boolean value, Element parent) { addAttribute(id.toString(), Boolean.toString(value), parent); } /** * Add an attribute with an {@code Enum.toString()} name and * {@code Double.toString()} value to an XML {@link Element}. * * @param id the name identifier of the attribute * @param value the value of the attribute * @param parent the {@code Element} to add attribute to */ public static void addAttribute(Enum<?> id, double value, Element parent) { addAttribute(id.toString(), Double.toString(value), parent); } /** * Add an attribute with an {@code Enum.toString()} name and * {@code Integer.toString()} value to an XML {@link Element}. * * @param id the name identifier of the attribute * @param value the value of the attribute * @param parent the {@code Element} to add attribute to */ public static void addAttribute(Enum<?> id, int value, Element parent) { addAttribute(id.toString(), Integer.toString(value), parent); } /** * Add an attribute with an {@code Enum.toString()} name and * {@code Arrays.toString(double[])} value to an XML {@link Element}. * * @param id the name identifier of the attribute * @param values the value of the attribute * @param parent the {@code Element} to add attribute to */ public static void addAttribute(Enum<?> id, double[] values, Element parent) { addAttribute(id.toString(), Arrays.toString(values), parent); } /** * Add an attribute with an {@code Enum.toString()} name and formatted * {@code double} value to an XML {@link Element}. Note that extra leading and * trailing zeros are removed from the formatted value per * {@link #stripZeros(String)}. * * @param id the attribute name identifier * @param value the {@code double} value of the attribute * @param format a format string * @param parent element to add attribute to * @see String#format(String, Object...) */ public static void addAttribute(Enum<?> id, double value, String format, Element parent) { addAttribute(id.toString(), stripZeros(String.format(format, value)), parent); } private static void addAttribute(String name, String value, Element parent) { parent.setAttribute(name, value); } /** * Add a child {@link Element} to a parent and return a reference to the * child. * * @param id the {@code Element} name identifier * @param parent {@code Element} for child */ public static Element addElement(Enum<?> id, Element parent) { return addElement(id.toString(), parent); } private static Element addElement(String name, Element parent) { Element e = parent.getOwnerDocument().createElement(name); parent.appendChild(e); return e; } /** * Add a child {@link Comment} to a parent and return a reference to the * comment. * * @param comment to add * @param parent {@code Element} for comment */ public static Comment addComment(String comment, Element parent) { Comment c = parent.getOwnerDocument().createComment(comment); parent.appendChild(c); return c; } /** * Read and return the value associated with an {@code Enum.toString()} name * from an {@link Attributes} container as a {@code boolean}. Method * explicitely checks that a case-insensitive value of "true" or "false" is * supplied (as opposed to simply defaulting to {@code false}. * * @param id the name identifier of the attribute * @param atts a SAX {@code Attributes} container * @throws NullPointerException if no attribute with the name * {@code id.toString()} exists */ public static boolean readBoolean(Enum<?> id, Attributes atts) { String name = checkNotNull(id).toString(); String value = checkNotNull(atts).getValue(name); validateAttribute(name, value); checkArgument(value.equalsIgnoreCase("true") || value.equalsIgnoreCase("false"), "Unparseable attribute " + id.toString() + "=\"" + value + "\""); return Boolean.valueOf(value); } /** * Read and return the value associated with an {@code Enum.toString()} name * from an {@link Attributes} container as an {@code int}. * * @param id the name identifier of the attribute * @param atts a SAX {@code Attributes} container * @throws NullPointerException if no attribute with the name * {@code id.toString()} exists */ public static int readInt(Enum<?> id, Attributes atts) { String name = checkNotNull(id).toString(); String value = checkNotNull(atts).getValue(name); try { return Integer.valueOf(validateAttribute(name, value)); } catch (NumberFormatException nfe) { throw createAttributeException(name, value); } } /** * Read and return the value associated with an {@code Enum.toString()} name * from an {@link Attributes} container as a {@code double}. * * @param id the name identifier of the attribute * @param atts a SAX {@code Attributes} container * @throws NullPointerException if no attribute with the name * {@code id.toString()} exists */ public static double readDouble(Enum<?> id, Attributes atts) { String name = checkNotNull(id).toString(); String value = checkNotNull(atts).getValue(name); try { return Double.valueOf(validateAttribute(name, value)); } catch (NumberFormatException nfe) { throw createAttributeException(name, value); } } /** * Read and return the value associated with an {@code Enum.toString()} name * from an {@link Attributes} container as a {@code double[]}. * * @param id the name identifier of the attribute * @param atts a SAX {@code Attributes} container * @throws NullPointerException if no attribute with the name * {@code id.toString()} exists */ public static double[] readDoubleArray(Enum<?> id, Attributes atts) { String name = checkNotNull(id).toString(); String value = checkNotNull(atts).getValue(name); try { return toDoubleArray(validateAttribute(name, value)); } catch (NumberFormatException nfe) { throw createAttributeException(name, value); } } /** * Read and return the value associated with an {@code Enum.toString()} name * from an {@link Attributes} container as a {@code String}. * * @param id the name identifier of the attribute * @param atts a SAX {@code Attributes} container * @throws NullPointerException if no attribute with the name * {@code id.toString()} exists */ public static String readString(Enum<?> id, Attributes atts) { String name = checkNotNull(id).toString(); String value = checkNotNull(atts).getValue(name); return validateAttribute(name, value); } /** * Read and return the value associated with an {@code Enum.toString()} name * from an {@link Attributes} container as an {@code Enum}. * * @param id the name identifier of the attribute * @param atts a SAX {@code Attributes} container * @param type the {@code class} of {@code enum} to return * @throws NullPointerException if no attribute with the name * {@code id.toString()} exists */ public static <T extends Enum<T>> T readEnum(Enum<?> id, Attributes atts, Class<T> type) { return Enum.valueOf(checkNotNull(type), readString(id, atts)); } private static String validateAttribute(String name, String value) { return checkNotNull(value, "Missing attribute '%s'", name); } private static IllegalArgumentException createAttributeException(String name, String value) { return new IllegalArgumentException( "Unparseable attribute: " + name + "=\"" + value + "\""); } /** * Returns a string containing the string representation of each of * {@code parts} joined with {@code delimiter}. * * @param parts the objects to join * @param delimiter the {@link Delimiter} to join on * @see Joiner */ public static String join(Iterable<?> parts, Delimiter delimiter) { return delimiter.joiner().join(parts); } /** * Split a {@code sequence} into string components and make them available * through a (possibly-lazy) {@code Iterator}. * * @param sequence the sequence of characters to split * @param delimiter the {@link Delimiter} to split on * @see Splitter */ public static Iterable<String> split(CharSequence sequence, Delimiter delimiter) { return delimiter.splitter().split(sequence); } /** * Split a {@code sequence} into string components and make them available * through an immutable {@code List}. * * @param sequence the sequence of characters to split * @param delimiter the {@link Delimiter} to split on */ public static List<String> splitToList(CharSequence sequence, Delimiter delimiter) { return delimiter.splitter().splitToList(sequence); } /** * Split {@code sequence} into {@code Double} components and make them * available through an immutable {@code List}. * * @param sequence the sequence of characters to split * @param delimiter the {@link Delimiter} to split on */ public static List<Double> splitToDoubleList(CharSequence sequence, Delimiter delimiter) { return FluentIterable .from(split(sequence, delimiter)) .transform(Doubles.stringConverter()) .toList(); } /** * Delimiter identifiers, each of which can provide a {@link Joiner} and * {@link Splitter}. */ public enum Delimiter { /** Colon (':') delimiter. */ COLON(':'), /** Comma (',') delimiter. */ COMMA(','), /** Dash ('-') delimiter. */ DASH('-'), /** Period ('.') delimiter. */ PERIOD('.'), /** Forward-slash ('/') delimiter. */ SLASH('/'), /** * Whitespace (' ') delimiter. * @see CharMatcher#WHITESPACE */ SPACE(' ', CharMatcher.WHITESPACE), /** Underscore ('_') delimiter. */ UNDERSCORE('_'); private Joiner joiner; private Splitter splitter; private Delimiter(char separator) { joiner = Joiner.on(separator).skipNulls(); splitter = Splitter.on(separator).omitEmptyStrings().trimResults(); } private Delimiter(char joinSeparator, CharMatcher splitMatcher) { joiner = Joiner.on(joinSeparator).skipNulls(); splitter = Splitter.on(splitMatcher).omitEmptyStrings().trimResults(); } /** * Returns a null-skipping {@link Joiner} on this {@code Delimiter}. * @see Joiner#skipNulls() */ public Joiner joiner() { return joiner; } /** * Returns an empty-string-omitting and result-trimming {@link Splitter} on * this {@code Delimiter}. * * @see Splitter#omitEmptyStrings() * @see Splitter#trimResults() */ public Splitter splitter() { return splitter; } } /** * Return a {@code String} representation of an {@code Iterable<Enum>} where * {@code Enum.name()} is used instead of {@code Enum.toString()}. * * @param iterable to process * @param enumClass */ public static <E extends Enum<E>> String enumsToString(Iterable<E> iterable, Class<E> enumClass) { return addBrackets(FluentIterable .from(iterable) .transform(Enums.stringConverter(enumClass).reverse()) .join(Delimiter.COMMA.joiner())); } /** * Convert an {@code Enum.name()} to a space-delimited presentation-friendly * string. * * @param e the {@code Enum} to generate label for * @param capitalize true if first letter of each word should be capitalized; * false if letters should all be lowercase */ public static String enumLabelWithSpaces(Enum<? extends Enum<?>> e, boolean capitalize) { return join(splitEnum(e, capitalize), Delimiter.SPACE); } /** * Convert an {@code Enum.name()} to a dash-delimited presentation-friendly * string. * * @param e the {@code Enum} to generate label for * @param capitalize true if fist letter of each word should be capitalized; * false if letters should all be lowercase */ public static String enumLabelWithDashes(Enum<? extends Enum<?>> e, boolean capitalize) { return join(splitEnum(e, capitalize), Delimiter.DASH); } /* Splits and possibly capitalizes an enum.name() */ private static List<String> splitEnum(Enum<? extends Enum<?>> e, boolean capitalize) { Iterable<String> sources = split(e.name(), Delimiter.UNDERSCORE); List<String> results = Lists.newArrayList(); for (String s : sources) { results.add(capitalize ? capitalize(s) : s.toLowerCase()); } return results; } /** * Capitalize supplied string by converting the first {@code char} to * uppercase and all subsequent {@code char}s to lowercase. * * @param s the string to capitalize */ public static String capitalize(String s) { return Character.toUpperCase(s.charAt(0)) + s.substring(1).toLowerCase(); } /** * Convert a bracketed and comma-delimited string of numbers (e.g. [1.0, 2.0, * 3.0] to a {@code double[]}. This is the reverse of * {@link Arrays#toString(double[])} and {@link AbstractCollection#toString()} * * @param s the string to convert */ public static double[] toDoubleArray(String s) { return Doubles.toArray(FluentIterable.from(split(trimEnds(s), Delimiter.COMMA)) .transform(Doubles.stringConverter()).toList()); } /** * Convert a {@code Collection<Double>} to a string of the same format * returned by {@link Arrays#toString(double[])} and * {@link AbstractCollection#toString()} , but will format the values using * the supplied format string. The supplied {@code format} should match that * expected by {@code String.format(String, Object...)} * * @param values the values to convert * @param format a format string */ public static String toString(Collection<Double> values, String format) { return addBrackets(join(Iterables.transform(values, new FormatDoubleFunction(format, true)), Delimiter.COMMA)); } /** * Convert a {@code Collection<Double>} to a string of the same format * returned by {@link Arrays#toString(double[])} and * {@link AbstractCollection#toString()} , but will format the values using * the supplied format string. The supplied {@code format} should match that * expected by {@code String.format(String, Object...)} * * @param values the values to convert * @param format a format string * @param delimiter the delimiter to use * @param brackets {@code true} if hard brackets should be added to start and * end, {@code false} otherwise */ public static String toString(Collection<Double> values, String format, String delimiter, boolean brackets, boolean cleanZeros) { String base = Joiner.on(delimiter).join( Iterables.transform(values, new FormatDoubleFunction(format, cleanZeros))); return brackets ? addBrackets(base) : base; } /** * Convert an ordered list of non-repeating {@code Integer}s to a more compact * string form. For example, * {@code List<Integer>.toString() = "[1, 2, 3, 4, 10, 19, 18, 17, 16]"} would * instead be written as {@code "1:4,10,19:16"}. * * @param values to convert * @throws IllegalArgumentException if {@code values} is empty or if * {@code values} contains adjacent repeating values * @see #rangeStringToIntList(String) */ public static String intListToRangeString(List<Integer> values) { checkArgument(checkNotNull(values).size() > 0, "values may not be empty"); // TODO do test for list containing single value List<int[]> ranges = Lists.newArrayList(); int start = values.get(0); int end = values.get(0); boolean buildingRange = false; boolean dir = true; for (int i = 1; i < values.size(); i++) { int next = values.get(i); checkArgument(next != end, "repeating value %s in %s", next, values); boolean currentDir = next > end; boolean terminateRange = // step > 1 (Math.abs(next - end) != 1) || // direction change (buildingRange && currentDir != dir) || // end of list (i == values.size() - 1); if (terminateRange) { if (i == values.size() - 1) { // singleton trailing value if (Math.abs(next - end) == 1 && currentDir == dir) { ranges.add(new int[] { start, next }); } else { ranges.add(new int[] { start, end }); ranges.add(new int[] { next, next }); } } else { ranges.add(new int[] { start, end }); } start = next; end = next; buildingRange = false; continue; } // starting or continuing new range buildingRange = true; dir = currentDir; end = next; } return join(Iterables.transform(ranges, IntArrayToString.INSTANCE), Delimiter.COMMA); } /** * Complement of {@link #intListToRangeString} converts a string of the form * {@code "[[1:4],10,[19:16]]"} to an ordered {@code List<Integer>} (e.g. * {@code [1, 2, 3, 4, 10, 19, 18, 17, 16]}). This method should only be * called with strings created by {@link #intListToRangeString}, otherwise * results are undefined. * * @param s the string to convert * @see #intListToRangeString */ public static List<Integer> rangeStringToIntList(String s) { Iterable<int[]> values = Iterables.transform(split(s, Delimiter.COMMA), StringToIntArray.INSTANCE); return Ints.asList(Ints.concat(Iterables.toArray(values, int[].class))); } // internal use only - no argument checking // writes 2-element int[]s as 'a:b' or just 'a' if a==b private enum IntArrayToString implements Function<int[], String> { INSTANCE; @Override public String apply(int[] ints) { return (ints[0] == ints[1]) ? Integer.toString(ints[0]) : ints[0] + ":" + ints[1]; } } // close complement of above, converts IntArrayToStrings to List<Integer>s private enum StringToIntArray implements Function<String, int[]> { INSTANCE; @Override public int[] apply(String s) { if (s.contains(":")) { Iterator<Integer> rangeIt = Iterators.transform( split(s, Delimiter.COLON).iterator(), Ints.stringConverter()); return Indexing.indices(rangeIt.next(), rangeIt.next()); } return new int[] { Integer.valueOf(s) }; } } /** * Strip trailing zeros of a decimal number that has already been formatted as * a string. Method will leave a single zero after a decimal. Method will not * alter exponential forms, which may have a critical '0' in last position. * * @param s the string to clean */ public static String stripZeros(String s) { if (s.charAt(1) == '.' && s.contains("e-")) { return s; } s = s.replaceAll("0*$", ""); if (s.endsWith(".")) { s += "0"; } return s; } /** * Returns a new {@code String} created by first trimming the supplied * {@code String} of leading and trailing whitespace (via * {@code String.trim()}), and then removing the first and last characters. At * a minimum, this method will return an empty string. * * @param s {@code String} to trim * @throws IllegalArgumentException if the size of the supplied string is * <2 after leading and trailing whitespace has been removed */ public static String trimEnds(String s) { String trimmed = s.trim(); checkArgument(trimmed.length() > 1, "\"%s\" is too short", s); return trimmed.substring(1, trimmed.length() - 1); } /* * Adds brackets to the supplied string. */ static String addBrackets(String s) { return '[' + s + ']'; } private static final Splitter.MapSplitter QUERY_SPLITTER = Splitter.on('&').trimResults() .withKeyValueSeparator('='); private static final Joiner.MapJoiner QUERY_JOINER = Joiner.on('&').withKeyValueSeparator("="); /** * Convert a {@code Map} to a {@code URL} name-value pair query string. * @param params the parameter map to convert */ public static String mapToQuery(Map<String, String> params) { return "?" + QUERY_JOINER.join(params); } /** * Convert a {@code URL} name-value pair query string to a {@code Map}. * @param query the string to convert */ public static Map<String, String> queryToMap(String query) { if (query.startsWith("?")) { query = query.substring(1); } return QUERY_SPLITTER.split(query); } /** * Read and return the {@code int} at {@code position} in a space-delimited * string. * * @param s the string to read from * @param position the position to read */ public static int readInt(String s, int position) { return Integer.valueOf(Iterables.get(split(s, Delimiter.SPACE), position)); } /** * Read and return the {@code double} at {@code position} in a space-delimited * string. * * @param s the string to read from * @param position the position to read */ public static double readDouble(String s, int position) { return Double.valueOf(Iterables.get(split(s, Delimiter.SPACE), position)); } /** * Strip a trailing comment from a string that starts with the supplied * character and return the cleaned string. * * @param s the string to process * @param c the comment indicator character */ public static String stripComment(String s, char c) { int index = s.indexOf(c); return index != -1 ? s.substring(0, index) : s; } /** * Read the specified number of strings from an {@code Iterator} and return * them as a {@code List}. * * @param it the string {@code Iterator} to read from * @param n the number of strings to read */ public static List<String> toLineList(Iterator<String> it, int n) { List<String> lines = new ArrayList<String>(); for (int i = 0; i < n; i++) { lines.add(it.next()); } return lines; } /** * Returns a {@link Function} for converting {@code double}s to formatted * strings. If a value to format is 0.0, the format string is ignored in favor * of always printing the often more compact string: "0.0". * * @param format a format string * @see String#format(String, Object...) */ public static Function<Double, String> formatDoubleFunction(String format) { return new FormatDoubleFunction(format, true); } private static class FormatDoubleFunction implements Function<Double, String> { private String format; private boolean cleanZeros; private FormatDoubleFunction(String format, boolean cleanZeros) { this.format = format; this.cleanZeros = cleanZeros; } @Override public String apply(Double value) { return (cleanZeros && value == 0.0) ? "0.0" : String.format(format, value); } } /** * Reads a binary {@code byte} stream as a sequence of {@code double}s. Method * closes the supplied {@code InputStream} before returning. * * @param in the {@code InputStream} to read from * @param byteCount number of bytes to read * @return a {@code List<Double>} * @throws IOException if there is a problem reading the supplied stream */ public static List<Double> readBinaryDoubleList(InputStream in, int byteCount) throws IOException { checkArgument(byteCount % 8 == 0, "byte count incompatible with doubles"); checkArgument(byteCount > 0, "byte count too small"); if (!(checkNotNull(in) instanceof BufferedInputStream)) { in = new BufferedInputStream(in); } DataInputStream din = new DataInputStream(in); int size = byteCount / 8; double[] vals = new double[size]; for (int i = 0; i < size; i++) { vals[i] = din.readDouble(); } in.close(); return Doubles.asList(vals); } /** * Reads a binary {@code byte} stream as a sequence of {@code int}s. Method * closes the supplied {@code InputStream} before returning. * * @param in the {@code InputStream} to read from * @return a {@code List} of {@code List<Integer>}s * @throws IOException if there is a problem reading the supplied * {@code InputStream} */ public static List<List<Integer>> readBinaryIntLists(InputStream in) throws IOException { if (!(checkNotNull(in) instanceof BufferedInputStream)) { in = new BufferedInputStream(in); } DataInputStream din = new DataInputStream(in); int count = din.readInt(); checkState(count > 0, "Number of lists must be > 0"); List<List<Integer>> list = Lists.newArrayList(); for (int i = 0; i < count; i++) { int[] vals = new int[din.readInt()]; for (int j = 0; j < vals.length; j++) { vals[j] = din.readInt(); } list.add(Ints.asList(vals)); } in.close(); return list; } /** * This method is currently unused and slated for removal. * * @param in * @param bitSetSize * @throws IOException */ @Deprecated public static List<BitSet> readBinaryIntBitSets(InputStream in, int bitSetSize) throws IOException { if (!(checkNotNull(in) instanceof BufferedInputStream)) { in = new BufferedInputStream(in); } DataInputStream din = new DataInputStream(in); int count = din.readInt(); checkState(count > 0, "Number of lists must be > 0"); List<BitSet> list = Lists.newArrayList(); for (int i = 0; i < count; i++) { int size = din.readInt(); BitSet bits = new BitSet(bitSetSize); for (int j = 0; j < size; j++) { bits.set(din.readInt()); } list.add(bits); } in.close(); return list; } }