use std::fs::File;
use std::io::Read;
use std::path::{Path, PathBuf};
use anyhow::anyhow;
use csv::StringRecord;
use derive_builder::Builder;
use encoding_rs::{Encoding, UTF_8};
use encoding_rs_io::DecodeReaderBytesBuilder;
use glob::glob;
use lindera_dictionary::dictionary::prefix_dictionary::PrefixDictionary;
use lindera_dictionary::viterbi::LexType;
use log::debug;
use lindera_dictionary::error::LinderaErrorKind;
use lindera_dictionary::LinderaResult;
use crate::dictionary::word_encoding::DictionaryWordEncoding;
pub mod details;
pub mod parser;
pub mod word_entry;
use self::details::generate_words_files;
use self::parser::{normalize, CSVParser};
use self::word_entry::{build_word_entry_map, generate_double_array, generate_values};
#[derive(Builder)]
#[builder(name = CSVReaderOptions)]
#[builder(build_fn(name = "builder"))]
pub struct CSVReader {
#[builder(default = "true")]
flexible_csv: bool,
#[builder(field(ty = "String", build = "get_encoding(&self.encoding)?"))]
encoding: &'static Encoding,
#[builder(default = "false")]
normalize_details: bool,
}
fn get_encoding(encoding_str: &str) -> Result<&'static Encoding, CSVReaderOptionsError> {
if encoding_str.is_empty() {
return Ok(UTF_8);
}
Encoding::for_label_no_replacement(encoding_str.as_bytes()).ok_or_else(|| {
CSVReaderOptionsError::ValidationError(format!("Invalid encoding: {}", encoding_str))
})
}
impl CSVReader {
pub fn load_csv_data(&self, input_dir: &Path) -> LinderaResult<Vec<StringRecord>> {
let filenames = self.collect_csv_files(input_dir)?;
let mut rows = self.read_csv_files(&filenames)?;
if self.normalize_details {
rows.sort_by_key(|row| normalize(&row[0]));
} else {
rows.sort_by(|a, b| a[0].cmp(&b[0]))
}
Ok(rows)
}
fn collect_csv_files(&self, input_dir: &Path) -> LinderaResult<Vec<PathBuf>> {
let pattern = if let Some(path) = input_dir.to_str() {
format!("{path}/*.csv")
} else {
return Err(LinderaErrorKind::Io
.with_error(anyhow::anyhow!("Failed to convert path to &str."))
.add_context(format!(
"Input directory path contains invalid characters: {input_dir:?}"
)));
};
let mut filenames: Vec<PathBuf> = Vec::new();
for entry in glob(&pattern).map_err(|err| {
LinderaErrorKind::Io
.with_error(anyhow::anyhow!(err))
.add_context(format!("Failed to glob CSV files with pattern: {pattern}"))
})? {
match entry {
Ok(path) => {
if let Some(filename) = path.file_name() {
filenames.push(Path::new(input_dir).join(filename));
} else {
return Err(LinderaErrorKind::Io
.with_error(anyhow::anyhow!("failed to get filename"))
.add_context(format!("Invalid filename in path: {path:?}")));
};
}
Err(err) => {
return Err(LinderaErrorKind::Content
.with_error(anyhow!(err))
.add_context(format!(
"Failed to process glob entry with pattern: {pattern}"
)));
}
}
}
Ok(filenames)
}
pub fn read_csv_files(&self, filenames: &[PathBuf]) -> LinderaResult<Vec<StringRecord>> {
let mut rows: Vec<StringRecord> = vec![];
for filename in filenames {
debug!("reading {filename:?}");
let file = File::open(filename).map_err(|err| {
LinderaErrorKind::Io
.with_error(anyhow::anyhow!(err))
.add_context(format!("Failed to open CSV file: {filename:?}"))
})?;
let reader: Box<dyn Read> = if self.encoding == UTF_8 {
Box::new(file)
} else {
Box::new(
DecodeReaderBytesBuilder::new()
.encoding(Some(self.encoding))
.build(file),
)
};
let mut rdr = csv::ReaderBuilder::new()
.has_headers(false)
.flexible(self.flexible_csv)
.from_reader(reader);
for result in rdr.records() {
let record = result.map_err(|err| {
LinderaErrorKind::Content
.with_error(anyhow!(err))
.add_context(format!("Failed to parse CSV record in file: {filename:?}"))
})?;
rows.push(record);
}
}
Ok(rows)
}
}
pub fn write_system_prefix_dictionary<P: CSVParser, E: DictionaryWordEncoding>(
parser: &P,
rows: &[StringRecord],
output_dir: &Path,
) -> LinderaResult<()> {
let word_entry_map = build_word_entry_map(parser, rows, LexType::System)?;
let mut dict_da_writer = File::create(output_dir.join("dict.da")).map_err(|err| {
LinderaErrorKind::Io
.with_error(anyhow::anyhow!(err))
.add_context("Failed to create dict.da file")
})?;
let da = generate_double_array(&word_entry_map, true)?;
write(&da, &mut dict_da_writer)?;
let mut dict_vals_writer = File::create(output_dir.join("dict.vals")).map_err(|err| {
LinderaErrorKind::Io
.with_error(anyhow::anyhow!(err))
.add_context("Failed to create dict.vals file")
})?;
let vals = generate_values(&word_entry_map)?;
write(&vals, &mut dict_vals_writer)?;
let mut dict_words_writer = File::create(output_dir.join("dict.words")).map_err(|err| {
LinderaErrorKind::Io
.with_error(anyhow::anyhow!(err))
.add_context("Failed to create dict.words file")
})?;
let mut dict_wordsidx_writer =
File::create(output_dir.join("dict.wordsidx")).map_err(|err| {
LinderaErrorKind::Io
.with_error(anyhow::anyhow!(err))
.add_context("Failed to create dict.wordsidx file")
})?;
let (words, wordsidx) = generate_words_files::<P, E>(parser, rows)?;
write(&words, &mut dict_words_writer)?;
write(&wordsidx, &mut dict_wordsidx_writer)?;
Ok(())
}
pub fn generate_user_prefix_dictionary<P: CSVParser, E: DictionaryWordEncoding>(
parser: &P,
rows: &[StringRecord],
) -> LinderaResult<PrefixDictionary> {
let word_entry_map = build_word_entry_map(parser, rows, LexType::System)?;
let da = generate_double_array(&word_entry_map, false)?;
let vals = generate_values(&word_entry_map)?;
let (words, wordsidx) = generate_words_files::<P, E>(parser, rows)?;
PrefixDictionary::load(da, vals, wordsidx, words, false)
}
fn write(data: &[u8], writer: &mut impl std::io::Write) -> LinderaResult<()> {
writer.write_all(data).map_err(|err| {
LinderaErrorKind::Io
.with_error(anyhow::anyhow!(err))
.add_context("Failed to write data to output")
})
}