/* * Copyright 2017 Pavel Fatin, https://pavelfatin.com * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.pavelfatin.typometer.data; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.text.ParseException; import java.util.*; import java.util.stream.Stream; import static java.lang.String.join; import static java.util.Arrays.stream; import static java.util.stream.Collectors.toList; class CsvData { private static final String QUOTE = "\""; private static final String FIELD_SEPARATOR = ","; private static final String LINE_SEPARATOR = "\r\n"; // RFC 4180 private static final DecimalFormat FIELD_FORMAT = createDecimalFormat(Locale.US, 2); private CsvData() { } private static DecimalFormat createDecimalFormat(Locale locale, int fractionDigits) { DecimalFormat result = new DecimalFormat(); result.setDecimalFormatSymbols(new DecimalFormatSymbols(locale)); result.setMinimumFractionDigits(fractionDigits); result.setMaximumFractionDigits(fractionDigits); result.setGroupingUsed(false); return result; } static void write(BufferedWriter writer, Collection<Column> columns) throws IOException { Collection<String> titles = columns.stream().map(Column::getTitle).collect(toList()); Collection<Collection<Double>> values = columns.stream().map(Column::getValues).collect(toList()); writeTitles(writer, titles); writeRecords(writer, values); } private static void writeTitles(BufferedWriter writer, Collection<String> titles) throws IOException { Collection<String> quotedTitles = titles.stream().map(CsvData::quote).collect(toList()); writer.write(join(FIELD_SEPARATOR, quotedTitles)); writer.write(LINE_SEPARATOR); } private static String quote(String s) { return s.chars().allMatch(Character::isLetterOrDigit) ? s : QUOTE + s + QUOTE; } private static void writeRecords(BufferedWriter writer, Collection<Collection<Double>> columns) throws IOException { Collection<Iterator<Double>> columnIterators = columns.stream().map(Collection::iterator).collect(toList()); while (true) { Collection<String> record = columnIterators.stream().map(CsvData::nextAsString).collect(toList()); if (record.stream().allMatch(String::isEmpty)) { break; } writer.write(join(FIELD_SEPARATOR, record)); writer.write(LINE_SEPARATOR); } } private static String nextAsString(Iterator<Double> it) { return it.hasNext() ? FIELD_FORMAT.format((double) it.next()) : ""; } static Collection<Column> read(BufferedReader reader) throws IOException { Collection<String> titles = readTitles(reader); Iterator<Collection<Double>> columnIterator = readRecords(reader, titles.size()).iterator(); return titles.stream().map(title -> new Column(title, columnIterator.next())).collect(toList()); } private static Collection<String> readTitles(BufferedReader reader) throws IOException { String line = reader.readLine(); if (line == null) { return Collections.emptyList(); } String[] row = line.split(FIELD_SEPARATOR); return stream(row).map(CsvData::unquote).collect(toList()); } private static String unquote(String s) { return s.startsWith(QUOTE) && s.endsWith(QUOTE) ? s.substring(1, s.length() - 1) : s; } private static Collection<Collection<Double>> readRecords(BufferedReader reader, int columnCount) throws IOException { List<Collection<Double>> columns = Stream.generate(() -> new ArrayList<Double>()).limit(columnCount).collect(toList()); while (true) { String line = reader.readLine(); if (line == null) { break; } Collection<Optional<Double>> record = stream(line.split(FIELD_SEPARATOR)).map(s -> parseField(s)).collect(toList()); int index = 0; for (Optional<Double> field : record) { field.ifPresent(columns.get(index)::add); index++; } } return columns; } private static Optional<Double> parseField(String s) { try { return s.isEmpty() ? Optional.empty() : Optional.of(FIELD_FORMAT.parse(s).doubleValue()); } catch (ParseException e) { throw new RuntimeException(e); } } }