use std::{
fmt::{self, Display, Formatter},
mem,
path::Path,
sync::Arc,
};
use diskit::{diskit_extend::DiskitExt, Diskit};
use crate::{
config::ArcConfig,
err::Error,
l10n::L10n,
songs::{L10nHelper, Song, Songs},
};
#[derive(Clone, Debug)]
pub struct Csv
{
pub entries: Vec<Vec<String>>,
}
#[derive(Debug, Clone, Copy)]
enum CsvParserState
{
StartOfEntry,
Normal,
EntryQuoted,
InEscaping,
}
impl Display for Csv
{
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error>
{
for line in &self.entries
{
let mut first = true;
for field in line
{
if !first
{
write!(f, ",")?;
}
first = false;
write!(f, "\"")?;
for c in field.chars()
{
match c
{
'\"' => write!(f, "\"\"")?,
_ => write!(f, "{}", c)?,
}
}
write!(f, "\"")?;
}
writeln!(f)?;
}
Ok(())
}
}
impl Csv
{
fn read_pseudo_csv(buf: &[u8]) -> Result<Self, Error>
{
let mut csv = vec![];
let mut entry = vec![];
let mut field = vec![];
let mut quoted = false;
for &c in buf
{
if quoted
{
field.push(c);
quoted = false;
}
else if c == b'\\'
{
quoted = true;
continue;
}
else if c == b','
{
entry.push(String::from_utf8(mem::take(&mut field))?);
}
else if c == b'\n'
{
if !field.is_empty()
{
return Err(Error::MalformattedSongsCsv);
}
csv.push(mem::take(&mut entry));
}
else
{
field.push(c);
}
}
Ok(Self { entries: csv })
}
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn new<P: AsRef<Path>, D>(path: P, diskit: D) -> Result<Self, Error>
where
D: Diskit,
{
#[allow(clippy::enum_glob_use)]
use CsvParserState::*;
let buf = diskit.read_to_string(path)?;
if let Ok(csv) = Self::read_pseudo_csv(buf.as_bytes())
{
return Ok(csv);
}
let mut csv = vec![];
let mut line = vec![];
let mut field = String::new();
let mut state = StartOfEntry;
for c in buf.chars()
{
match (state, c)
{
(StartOfEntry, '"') => state = EntryQuoted,
(Normal, '"') => return Err(Error::MalformattedSongsCsv),
(StartOfEntry | Normal | InEscaping, ',') =>
{
line.push(mem::take(&mut field));
state = StartOfEntry;
}
(StartOfEntry | Normal | InEscaping, '\n') =>
{
if field.is_empty() && line.is_empty()
{
continue;
}
line.push(mem::take(&mut field));
csv.push(mem::take(&mut line));
state = StartOfEntry;
}
(StartOfEntry | Normal, _) =>
{
field.push(c);
state = Normal;
}
(EntryQuoted, '"') => state = InEscaping,
(EntryQuoted, _) | (InEscaping, '"') =>
{
field.push(c);
state = EntryQuoted;
}
(InEscaping, _) => return Err(Error::MalformattedSongsCsv),
}
}
Ok(Self { entries: csv })
}
#[must_use]
pub(crate) fn from<D>(songs: &Songs<D>) -> Self
where
D: Diskit,
{
let x = songs
.songs
.iter()
.map(|song| {
vec![
song.name.clone(),
song.num.to_string(),
song.loud.to_string(),
]
})
.collect();
Self { entries: x }
}
#[must_use]
pub(crate) fn get_songs<D>(self, config: Arc<ArcConfig>, l10n: L10n, diskit: D) -> Option<Songs<D>>
where
D: Diskit,
{
if self.entries.iter().all(|song| song.len() == 3)
{
Some(Songs {
songs: self
.entries
.into_iter()
.map(|mut song| Song {
name: mem::take(&mut song[0]),
num: song[1].parse().unwrap(),
loud: song[2].parse().unwrap(),
})
.collect(),
config,
l10n_helper: L10nHelper::new(l10n),
diskit,
})
}
else
{
None
}
}
}