iso15924 0.1.0

ISO 15924 data.
Documentation
//! Parsing functionality for parsing rows of script records.
//!
//! This is designed to parse data coming from the [`DATA_URL`] source.
//!
//! [`DATA_URL`]: ../../const.DATA_URL.html

use crate::{ScriptDate, ScriptDateError};
use std::{
    error::Error,
    fmt::{Display, Formatter, Result as FmtResult},
    num::ParseIntError,
    str::FromStr,
};
use super::ScriptCode;

/// A column of a row, used for when parsing the column value fails.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub enum Column {
    /// The alias column is missing from the row.
    Alias,
    /// The code column is missing from the row.
    Code,
    /// The date column is missing from the row.
    Date,
    /// The english name column is missing from the row.
    NameEnglish,
    /// The french name column is missing from the row.
    NameFrench,
    /// The number column is missing from the row.
    Number,
    /// The unicode version column is missing from the row.
    UnicodeVersion,
}

/// The parts of a unicode version,used for when parsing the version fails.
#[derive(Clone, Copy, Debug)]
pub enum VersionPart {
    /// The major (and minor) part of the unicode version is missing from the
    /// row.
    Major,
    /// The minor part of the unicode version is missing from the row.
    Minor,
}

/// Error returned when parsing a row fails, for reasons such as a missing
/// column or invalid data format.
#[derive(Clone, Debug)]
pub enum ParseError {
    /// A column is missing from the row.
    ///
    /// The name of the column is provided.
    ColumnMissing(Column),
    /// Parsing a value to an integer failed.
    Integer {
        /// The column that couldn't be parsed.
        column: Column,
        /// The reason that parsing failed.
        source: ParseIntError,
        /// The value that couldn't be parsed.
        value: String,
    },
    /// Parsing a date failed due to improper date format.
    InvalidDate {
        /// The reason that parsing failed.
        source: ScriptDateError,
        /// The date value that couldn't be parsed.
        value: String,
    },
    /// A part of the unicode version is missing.
    MissingVersionPart {
        /// The part ([`Major`] or [`Minor`]) that is missing.
        ///
        /// [`Major`]: enum.VersionPart.html#variant.Major
        /// [`Minor`]: enum.VersionPart.html#variant.Minor
        part: VersionPart,
        /// value that couldn't be parsed.
        value: String,
    },
}

impl Display for ParseError {
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
        f.write_str(self.description())
    }
}

impl Error for ParseError {
    fn description(&self) -> &str {
        use self::ParseError::*;

        match self {
            ColumnMissing(_) => "A column was missing from the row",
            Integer { .. } => "An integer couldn't be parsed",
            InvalidDate { .. } => "A date in the data was invalid",
            MissingVersionPart { .. } => "Only a partial amount of the unicode version is present",
        }
    }
}

/// Returns a lazily parsing iterator over the provided lines of input.
pub fn parse_lines<'a>(
    input: &'a str,
) -> impl Iterator<Item = Result<ScriptCode<'a>, ParseError>> + 'a {
    input.lines().filter_map(|line| {
        if line.starts_with('#') || line.trim().is_empty() {
            return None;
        }

        Some(parse_line(line))
    })
}

/// Parses a single row of data.
pub fn parse_line(line: &str) -> Result<ScriptCode<'_>, ParseError> {
    let mut columns = line.split(';');

    // Code;N°;English Name;Nom français;PVA;Unicode Version;Date
    let code = columns.next().ok_or(ParseError::ColumnMissing(Column::Code))?;
    let num = columns.next().ok_or(ParseError::ColumnMissing(Column::Number))?;
    let name_en = columns.next().ok_or(ParseError::ColumnMissing(Column::NameEnglish))?;
    let name_fr = columns.next().ok_or(ParseError::ColumnMissing(Column::NameFrench))?;
    let alias = match columns.next() {
        Some("") => None,
        Some(v) => Some(v),
        None => return Err(ParseError::ColumnMissing(Column::Alias)),
    };
    let version = match columns.next() {
        Some("") => None,
        Some(version) => {
            let mut parts = version.split('.');
            let major_str = parts.next().ok_or_else(|| ParseError::MissingVersionPart {
                part: VersionPart::Major,
                value: version.to_owned(),
            })?;
            let major = parse_version_part(major_str)?;
            let minor_str = parts.next().ok_or_else(|| ParseError::MissingVersionPart {
                part: VersionPart::Minor,
                value: version.to_owned(),
            })?;
            let minor = parse_version_part(minor_str)?;

            Some((major, minor))
        },
        None => return Err(ParseError::ColumnMissing(Column::UnicodeVersion)),
    };
    let date = columns.next().ok_or(ParseError::ColumnMissing(Column::Date))?;
    let date = ScriptDate::from_str(&date).map_err(|source| ParseError::InvalidDate {
        source,
        value: date.to_owned(),
    })?;

    Ok(ScriptCode {
        alias: alias.map(Into::into),
        code: code.into(),
        date,
        name: name_en.into(),
        name_french: name_fr.into(),
        num: num.into(),
        unicode_version: version,
    })
}

fn parse_version_part(
    part: &str,
) -> Result<u8, ParseError> {
    part.parse().map_err(|source| ParseError::Integer {
        column: Column::UnicodeVersion,
        source,
        value: part.to_owned(),
    })
}