use std::{any::Any, fmt, fs, io, num::ParseIntError};
use hashbrown::{HashMap, HashSet};
use log::info;
use crate::{
block::{
base::{
clues_from_solution,
color::{ColorId, ColorPalette, ColorValue},
},
binary::BinaryBlock,
Block, Description,
},
board::Board,
utils::{iter::FindOk, product, rc::MutRc, split_sections},
};
pub use self::{ini::MyFormat, xml::WebPbn};
#[derive(Debug)]
pub struct ParseError(pub String);
pub trait BoardParser: fmt::Debug {
fn with_content(content: &str) -> Result<Self, ParseError>
where
Self: Sized;
fn parse<B>(&self) -> Board<B>
where
B: Block;
fn parse_rc<B>(&self) -> MutRc<Board<B>>
where
B: Block,
{
MutRc::new(self.parse())
}
fn infer_scheme(&self) -> PuzzleScheme;
}
impl From<io::Error> for ParseError {
fn from(err: io::Error) -> Self {
Self(format!("{:?}", err))
}
}
pub trait LocalReader: BoardParser {
fn read_local(file_name: &str) -> Result<Self, ParseError>
where
Self: Sized,
{
let content = Self::file_content(file_name)?;
Self::with_content(&content)
}
fn file_content(file_name: &str) -> io::Result<String> {
fs::read_to_string(file_name)
}
}
#[cfg(feature = "web")]
impl From<reqwest::Error> for ParseError {
fn from(err: reqwest::Error) -> Self {
Self(format!("{:?}", err))
}
}
pub trait NetworkReader: BoardParser {
fn read_remote(file_name: &str) -> Result<Self, ParseError>
where
Self: Sized,
{
let content = Self::http_content(file_name)?;
Self::with_content(&content)
}
#[cfg(feature = "web")]
fn http_content(url: &str) -> Result<String, reqwest::Error> {
info!("Requesting {} ...", url);
let response = reqwest::blocking::get(url)?;
response.text()
}
#[cfg(not(feature = "web"))]
fn http_content(url: &str) -> Result<String, ParseError> {
info!("Requesting {} ...", url);
Err(ParseError(format!(
"Cannot request url {}: no support for web client (hint: add --features=web)",
url
)))
}
}
pub trait Paletted {
fn get_colors(&self) -> Vec<(String, char, String)>;
fn get_colors_sorted(&self) -> Vec<(String, char, String)> {
let mut colors = self.get_colors();
colors.sort_unstable_by(|(name1, ..), (name2, ..)| name1.cmp(name2));
colors
}
fn default_palette(&self, white_name: &str, black_name: &str) -> ColorPalette {
let mut palette = ColorPalette::with_white_and_black(white_name, black_name);
for (name, symbol, value) in &self.get_colors_sorted() {
let val = ColorValue::parse(value);
palette.color_with_name_value_and_symbol(name, val, *symbol);
}
palette
}
fn get_palette(&self) -> ColorPalette;
}
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum PuzzleScheme {
BlackAndWhite,
MultiColor,
}
#[cfg(feature = "ini")]
mod ini {
use serde::Deserialize;
use super::{
Block, Board, BoardParser, ColorPalette, Description, LocalReader, Paletted, ParseError,
PuzzleScheme,
};
#[derive(Debug, Deserialize)]
struct Clues {
rows: String,
columns: String,
}
#[derive(Debug, Deserialize)]
struct Colors {
defs: Option<Vec<String>>,
}
#[derive(Debug, Deserialize)]
pub struct MyFormat {
clues: Clues,
colors: Option<Colors>,
}
impl LocalReader for MyFormat {}
impl From<toml::de::Error> for ParseError {
fn from(err: toml::de::Error) -> Self {
Self(format!("{:?}", err))
}
}
impl BoardParser for MyFormat {
fn with_content(content: &str) -> Result<Self, ParseError> {
Ok(toml::from_str(content)?)
}
fn parse<B>(&self) -> Board<B>
where
B: Block,
{
let clues = &self.clues;
let palette = self.get_palette();
Board::with_descriptions_and_palette(
Self::parse_clues(&clues.rows, &palette),
Self::parse_clues(&clues.columns, &palette),
Some(palette),
)
}
fn infer_scheme(&self) -> PuzzleScheme {
if let Some(colors) = &self.colors {
if let Some(defs) = &colors.defs {
if !defs.is_empty() {
return PuzzleScheme::MultiColor;
}
}
}
PuzzleScheme::BlackAndWhite
}
}
impl MyFormat {
fn parse_block<B>(block: &str, palette: &ColorPalette) -> B
where
B: Block,
{
let mut as_chars = block.chars();
let value_color_pos = as_chars.position(|c| !c.is_digit(10));
#[allow(clippy::option_if_let_else)]
let (value, block_color) = if let Some(pos) = value_color_pos {
let (value, color) = block.split_at(pos);
(value, Some(color))
} else {
(block, palette.get_default())
};
let color_id = block_color.and_then(|name| palette.id_by_name(name));
B::from_str_and_color(value, color_id)
}
fn parse_line<B>(descriptions: &str, palette: &ColorPalette) -> Option<Vec<Description<B>>>
where
B: Block,
{
let descriptions = descriptions.trim();
let non_comment: &str = descriptions
.split(&['#', ';'][..])
.next()
.expect("Split returned empty");
if non_comment.is_empty() {
return None;
}
Some(
non_comment
.split(',')
.filter_map(|row| {
let row = row.trim().trim_matches(&['\'', '"'][..]);
if row.is_empty() {
None
} else {
Some(Description::new(
row.split_whitespace()
.map(|block| Self::parse_block(block, palette))
.collect(),
))
}
})
.collect(),
)
}
pub(super) fn parse_clues<B>(
descriptions: &str,
palette: &ColorPalette,
) -> Vec<Description<B>>
where
B: Block,
{
descriptions
.lines()
.flat_map(|line| Self::parse_line(line, palette).unwrap_or_default())
.collect()
}
pub fn parse_color_def(color_def: impl AsRef<str>) -> (String, char, String) {
let parts: Vec<_> = color_def.as_ref().split('=').map(str::trim).collect();
let name = parts[0];
let mut desc = parts[1].to_string();
let symbol = desc.pop().expect("Empty color description in definition");
desc = desc.trim().trim_matches(&['(', ')'][..]).to_string();
(name.to_string(), symbol, desc)
}
}
impl Paletted for MyFormat {
fn get_colors(&self) -> Vec<(String, char, String)> {
if let Some(colors) = &self.colors {
if let Some(defs) = &colors.defs {
return defs.iter().map(Self::parse_color_def).collect();
}
}
vec![]
}
fn get_palette(&self) -> ColorPalette {
self.default_palette("W", "B")
}
}
}
#[cfg(not(feature = "ini"))]
mod ini {
use super::{Block, Board, BoardParser, ParseError, PuzzleScheme};
#[derive(Debug, Clone, Copy)]
pub struct MyFormat;
impl MyFormat {
const NO_FEATURE_ENABLED_MSG: &'static str =
"Cannot parse TOML-based puzzles: no support for TOML (hint: add --features=ini)";
}
impl BoardParser for MyFormat {
fn with_content(_content: &str) -> Result<Self, ParseError>
where
Self: Sized,
{
Err(ParseError(Self::NO_FEATURE_ENABLED_MSG.to_string()))
}
fn parse<B>(&self) -> Board<B>
where
B: Block,
{
unimplemented!("{}", Self::NO_FEATURE_ENABLED_MSG)
}
fn infer_scheme(&self) -> PuzzleScheme {
unimplemented!("{}", Self::NO_FEATURE_ENABLED_MSG)
}
}
}
#[cfg(feature = "xml")]
mod xml {
use sxd_document as xml;
use sxd_xpath::{
evaluate_xpath,
nodeset::{Node, Nodeset},
Value,
};
use crate::utils::rc::{mutate_ref, read_ref, InteriorMutableRef};
use super::{
Block, Board, BoardParser, ColorPalette, Description, LocalReader, NetworkReader, Paletted,
ParseError, PuzzleScheme,
};
#[derive(Debug)]
pub struct WebPbn {
package: xml::Package,
cached_colors: InteriorMutableRef<Option<Vec<(String, char, String)>>>,
cached_palette: InteriorMutableRef<Option<ColorPalette>>,
}
impl LocalReader for WebPbn {}
impl NetworkReader for WebPbn {
fn read_remote(file_name: &str) -> Result<Self, ParseError> {
let url = format!("{}/XMLpuz.cgi?id={}", Self::BASE_URL, file_name);
let content = Self::http_content(&url)?;
Self::with_content(&content)
}
}
impl From<xml::parser::Error> for ParseError {
fn from(err: xml::parser::Error) -> Self {
Self(format!("{:?}", err))
}
}
impl BoardParser for WebPbn {
fn with_content(content: &str) -> Result<Self, ParseError> {
let package = xml::parser::parse(content)?;
Ok(Self {
package,
cached_colors: InteriorMutableRef::new(None),
cached_palette: InteriorMutableRef::new(None),
})
}
fn parse<B>(&self) -> Board<B>
where
B: Block,
{
Board::with_descriptions_and_palette(
self.parse_clues("rows"),
self.parse_clues("columns"),
Some(self.get_palette()),
)
}
fn infer_scheme(&self) -> PuzzleScheme {
let colors = self.get_colors_sorted();
let names: Vec<_> = colors.iter().map(|(name, ..)| name).collect();
if names.is_empty() || names == ["black", "white"] {
return PuzzleScheme::BlackAndWhite;
}
PuzzleScheme::MultiColor
}
}
impl WebPbn {
const BASE_URL: &'static str = "http://webpbn.com";
fn parse_block<B>(block: &Node<'_>, palette: &ColorPalette) -> B
where
B: Block,
{
let value = block.string_value();
let block_color = if let Node::Element(e) = block {
e.attribute("color")
.map(|color| color.value())
.or_else(|| palette.get_default())
} else {
None
};
let color_id = block_color.and_then(|name| palette.id_by_name(name));
B::from_str_and_color(&value, color_id)
}
fn parse_line<B>(description: &Node<'_>, palette: &ColorPalette) -> Description<B>
where
B: Block,
{
Description::new(
description
.children()
.iter()
.filter_map(|child| {
if let Node::Text(_text) = child {
None
} else {
Some(Self::parse_block(child, palette))
}
})
.collect(),
)
}
fn get_clues<B>(descriptions: &Nodeset<'_>, palette: &ColorPalette) -> Vec<Description<B>>
where
B: Block,
{
descriptions
.document_order()
.iter()
.map(|line_node| Self::parse_line(line_node, palette))
.collect()
}
fn parse_clues<B>(&self, type_: &str) -> Vec<Description<B>>
where
B: Block,
{
let document = self.package.as_document();
let value = evaluate_xpath(&document, &format!(".//clues[@type='{}']/line", type_))
.expect("XPath evaluation failed");
if let Value::Nodeset(ns) = value {
Self::get_clues(&ns, &self.get_palette())
} else {
vec![]
}
}
}
impl WebPbn {
fn _get_colors(&self) -> Vec<(String, char, String)> {
let document = self.package.as_document();
let value = evaluate_xpath(&document, ".//color").expect("XPath evaluation failed");
if let Value::Nodeset(ns) = value {
ns.iter()
.filter_map(|color_node| {
let value = color_node.string_value();
if let Node::Element(e) = color_node {
let name = e
.attribute("name")
.expect("Not found 'name' attribute in the 'color' element")
.value();
let symbol = e
.attribute("char")
.expect("Not found 'char' attribute in the 'color' element")
.value();
let symbol: char = symbol.as_bytes()[0] as char;
Some((name.to_string(), symbol, value))
} else {
None
}
})
.collect()
} else {
vec![]
}
}
fn get_default_color(&self) -> Option<String> {
let document = self.package.as_document();
let value = evaluate_xpath(&document, ".//puzzle[@type='grid']")
.expect("XPath evaluation failed");
if let Value::Nodeset(ns) = value {
let first_node = ns.iter().next();
if let Some(Node::Element(e)) = first_node {
return e
.attribute("defaultcolor")
.map(|color| color.value().to_string());
}
}
None
}
fn _get_palette(&self) -> ColorPalette {
let mut palette = self.default_palette("white", "black");
if let Some(default_color) = self.get_default_color() {
palette.set_default(&default_color).unwrap();
}
palette
}
}
impl Paletted for WebPbn {
fn get_colors(&self) -> Vec<(String, char, String)> {
if let Some(colors) = read_ref(&self.cached_colors).as_ref() {
return colors.clone();
}
let result = self._get_colors();
let mut cache = mutate_ref(&self.cached_colors);
*cache = Some(result.clone());
result
}
fn get_palette(&self) -> ColorPalette {
if let Some(palette) = read_ref(&self.cached_palette).as_ref() {
return palette.clone();
}
let result = self._get_palette();
let mut cache = mutate_ref(&self.cached_palette);
*cache = Some(result.clone());
result
}
}
}
#[cfg(not(feature = "xml"))]
mod xml {
use super::{Block, Board, BoardParser, NetworkReader, ParseError, PuzzleScheme};
#[derive(Debug, Clone, Copy)]
pub struct WebPbn;
impl WebPbn {
const NO_FEATURE_ENABLED_MSG: &'static str =
"Cannot parse XML-based puzzles: no support for XML (hint: add --features=xml)";
}
impl BoardParser for WebPbn {
fn with_content(_content: &str) -> Result<Self, ParseError>
where
Self: Sized,
{
Err(ParseError(Self::NO_FEATURE_ENABLED_MSG.to_string()))
}
fn parse<B>(&self) -> Board<B>
where
B: Block,
{
unimplemented!("{}", Self::NO_FEATURE_ENABLED_MSG)
}
fn infer_scheme(&self) -> PuzzleScheme {
unimplemented!("{}", Self::NO_FEATURE_ENABLED_MSG)
}
}
impl NetworkReader for WebPbn {}
}
type EncodedInt = u16;
#[derive(Debug)]
pub struct NonogramsOrg {
encoded: Vec<Vec<EncodedInt>>,
}
impl NonogramsOrg {
const URLS: [&'static str; 2] = ["http://www.nonograms.ru/", "http://www.nonograms.org/"];
const PATHS: [(PuzzleScheme, &'static str); 2] = [
(PuzzleScheme::BlackAndWhite, "nonograms"),
(PuzzleScheme::MultiColor, "nonograms2"),
];
const CYPHER_PREFIX: &'static str = r"var d=";
const CYPHER_SUFFIX: char = ';';
fn extract_encoded_json(html: &str) -> Option<&str> {
#[allow(unused_imports)]
use crate::utils::Stripper;
html.lines().find_map(|line| {
line.strip_prefix(Self::CYPHER_PREFIX)
.map(|line| line.trim_end_matches(Self::CYPHER_SUFFIX))
})
}
fn parse_line(line: &str) -> Vec<EncodedInt> {
line.split(',')
.map(|x| x.parse().expect("The items should be positive integers"))
.collect()
}
fn parse_json(array: &str) -> Vec<Vec<EncodedInt>> {
array
.trim_start_matches('[')
.trim_end_matches(']')
.split("],[")
.map(Self::parse_line)
.collect()
}
#[allow(clippy::shadow_unrelated)]
#[allow(unknown_lints)]
#[allow(clippy::no_effect_underscore_binding)]
pub fn decipher(&self) -> (Vec<String>, Vec<Vec<ColorId>>) {
let cyphered = self.encoded();
let x = &cyphered[1];
let width = (x[0] % x[3] + x[1] % x[3] - x[2] % x[3]) as usize;
let x = &cyphered[2];
let height = (x[0] % x[3] + x[1] % x[3] - x[2] % x[3]) as usize;
let x = &cyphered[3];
let colors_number = (x[0] % x[3] + x[1] % x[3] - x[2] % x[3]) as usize;
let x = &cyphered[4];
let colors: Vec<_> = (0..colors_number)
.map(|c| {
let color_x = &cyphered[c + 5];
let a = color_x[0] - x[1];
let b = u32::from(color_x[1] - x[0]);
let c = u32::from(color_x[2] - x[3]);
let _unknown_flag = color_x[3] - a - x[2];
let a = &format!("{:x}", a + 256)[1..];
let b = &format!("{:x}", ((b + 256) << 8) + c)[1..];
a.to_string() + b
})
.collect();
let mut solution = vec![vec![0; width]; height];
let z = colors_number + 5;
let x = &cyphered[z];
let solution_size = (x[0] % x[3] * (x[0] % x[3]) + x[1] % x[3] * 2 + x[2] % x[3]) as usize;
let x = &cyphered[z + 1];
for i in 0..solution_size {
let y = &cyphered[z + 2 + i];
let vv = y[0] - x[0] - 1;
for j in 0..(y[1] - x[1]) {
let v = (j + vv) as usize;
let xx = y[3] - x[3] - 1;
solution[xx as usize][v] = ColorId::from(y[2] - x[2]);
}
}
(colors, solution)
}
pub fn encoded(&self) -> &[Vec<EncodedInt>] {
&self.encoded
}
fn get_solution_matrix(&self) -> Vec<Vec<ColorId>> {
let (_colors, solution_matrix) = self.decipher();
let palette = self.get_palette();
let mut mapping_cache = HashMap::new();
solution_matrix
.iter()
.map(|row| {
row.iter()
.map(|&item| {
*mapping_cache.entry(item).or_insert_with(|| {
palette
.id_by_name(&Self::color_name_by_id(item))
.unwrap_or(0)
})
})
.collect()
})
.collect()
}
fn color_name_by_id(id: ColorId) -> String {
format!("color-{}", id)
}
}
impl LocalReader for NonogramsOrg {}
impl Default for ParseError {
fn default() -> Self {
Self("Unknown parser error".to_string())
}
}
impl NetworkReader for NonogramsOrg {
fn read_remote(file_name: &str) -> Result<Self, ParseError> {
product(&Self::URLS, &Self::PATHS)
.iter()
.first_ok(|(base_url, (_scheme, path))| {
let url = format!("{}{}/i/{}", base_url, path, file_name);
let content = Self::http_content(&url)?;
Self::with_content(&content)
})
}
}
impl BoardParser for NonogramsOrg {
fn with_content(content: &str) -> Result<Self, ParseError> {
let json = Self::extract_encoded_json(content)
.ok_or_else(|| ParseError("Not found cypher in HTML content".to_string()))?;
Ok(Self {
encoded: Self::parse_json(json),
})
}
fn parse<B>(&self) -> Board<B>
where
B: Block,
{
let solution_matrix = self.get_solution_matrix();
let (columns, rows) = clues_from_solution(&solution_matrix, 0);
Board::with_descriptions_and_palette(rows, columns, Some(self.get_palette()))
}
fn infer_scheme(&self) -> PuzzleScheme {
let (colors, _solution) = self.decipher();
if colors.len() == 1 {
assert_eq!(colors, ["000000"]);
return PuzzleScheme::BlackAndWhite;
}
PuzzleScheme::MultiColor
}
}
impl Paletted for NonogramsOrg {
#[allow(clippy::cast_possible_truncation)]
fn get_colors(&self) -> Vec<(String, char, String)> {
let (colors, _solution) = self.decipher();
colors
.into_iter()
.enumerate()
.map(|(i, rgb)| (Self::color_name_by_id((i + 1) as ColorId), '?', rgb))
.collect()
}
fn get_palette(&self) -> ColorPalette {
let mut palette = ColorPalette::with_white("W");
for (name, _dumb_symbol, value) in &self.get_colors() {
let val = ColorValue::parse(value);
palette.color_with_name_and_value(name, val);
}
palette
}
}
#[derive(Debug)]
enum ParserKind {
Toml,
WebPbn,
NonogramsOrg,
Olsak,
Simple,
}
pub struct DetectedParser {
parser_kind: ParserKind,
inner: Box<dyn Any>,
}
impl DetectedParser {
fn cast<T>(&self) -> &T
where
T: BoardParser + 'static,
{
let expect_msg = format!("Parser should be created with {:?}", self.parser_kind);
self.inner.downcast_ref::<T>().expect(&expect_msg)
}
}
impl BoardParser for DetectedParser {
fn with_content(content: &str) -> Result<Self, ParseError> {
let trim_content = content.trim();
Ok(if trim_content.starts_with("<?xml") {
Self {
parser_kind: ParserKind::WebPbn,
inner: Box::new(WebPbn::with_content(content)?),
}
} else if ["<!DOCTYPE HTML", "<html", NonogramsOrg::CYPHER_PREFIX]
.iter()
.any(|&prefix| trim_content.starts_with(prefix))
{
Self {
parser_kind: ParserKind::NonogramsOrg,
inner: Box::new(NonogramsOrg::with_content(content)?),
}
} else {
let lines: Vec<_> = trim_content.lines().map(str::trim).collect();
if lines.contains(&"[clues]") {
Self {
parser_kind: ParserKind::Toml,
inner: Box::new(MyFormat::with_content(content)?),
}
} else if lines.contains(&": rows") {
Self {
parser_kind: ParserKind::Olsak,
inner: Box::new(OlsakParser::with_content(content)?),
}
} else {
Self {
parser_kind: ParserKind::Simple,
inner: Box::new(SimpleParser::with_content(content)?),
}
}
})
}
fn parse<B>(&self) -> Board<B>
where
B: Block,
{
match self.parser_kind {
ParserKind::Toml => self.cast::<MyFormat>().parse::<B>(),
ParserKind::WebPbn => self.cast::<WebPbn>().parse::<B>(),
ParserKind::NonogramsOrg => self.cast::<NonogramsOrg>().parse::<B>(),
ParserKind::Olsak => self.cast::<OlsakParser>().parse::<B>(),
ParserKind::Simple => self.cast::<SimpleParser>().parse::<B>(),
}
}
fn infer_scheme(&self) -> PuzzleScheme {
match self.parser_kind {
ParserKind::Toml => self.cast::<MyFormat>().infer_scheme(),
ParserKind::WebPbn => self.cast::<WebPbn>().infer_scheme(),
ParserKind::NonogramsOrg => self.cast::<NonogramsOrg>().infer_scheme(),
ParserKind::Olsak => self.cast::<OlsakParser>().infer_scheme(),
ParserKind::Simple => self.cast::<SimpleParser>().infer_scheme(),
}
}
}
impl fmt::Debug for DetectedParser {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
let inner = match self.parser_kind {
ParserKind::Toml => format!("{:?}", self.cast::<MyFormat>()),
ParserKind::WebPbn => format!("{:?}", self.cast::<WebPbn>()),
ParserKind::NonogramsOrg => format!("{:?}", self.cast::<NonogramsOrg>()),
ParserKind::Olsak => format!("{:?}", self.cast::<OlsakParser>()),
ParserKind::Simple => format!("{:?}", self.cast::<SimpleParser>()),
};
f.debug_struct("DetectedParser")
.field("parser_kind", &self.parser_kind)
.field("inner", &inner)
.finish()
}
}
#[derive(Debug, PartialEq)]
pub struct OlsakColor {
pub block_name: String,
pub symbol: char,
pub rgb: String,
pub name: String,
}
impl OlsakColor {
pub fn parse(color_def: &str) -> Self {
let parts: Vec<_> = color_def.split_whitespace().collect();
let block_name_and_symbol: Vec<_> = parts[0].split(':').collect();
let block_name = block_name_and_symbol[0];
let symbol = block_name_and_symbol[1].chars().next().unwrap_or(' ');
Self {
block_name: block_name.to_string(),
symbol,
rgb: parts[1].to_string(),
name: parts[2].to_string(),
}
}
}
#[derive(Debug)]
struct OlsakParser {
rows: Vec<Vec<String>>,
columns: Vec<Vec<String>>,
colors: HashMap<String, OlsakColor>,
}
impl From<String> for ParseError {
fn from(err: String) -> Self {
Self(err)
}
}
impl BoardParser for OlsakParser {
fn with_content(content: &str) -> Result<Self, ParseError>
where
Self: Sized,
{
let names = [": rows", ": columns", "#d"];
let mut sections = split_sections(content, &names, false, None)?;
let mut splitted: HashMap<_, _> = sections
.iter()
.map(|(&name, lines)| {
(
name,
lines
.iter()
.map(|&line| line.split_whitespace().map(ToString::to_string).collect())
.collect(),
)
})
.collect();
let colors = sections.remove(names[2]).unwrap_or_default();
Ok(Self {
rows: splitted.remove(names[0]).expect("Rows section not found"),
columns: splitted
.remove(names[1])
.expect("Columns section not found"),
colors: colors
.into_iter()
.map(|line| {
let color = OlsakColor::parse(line);
(color.block_name.clone(), color)
})
.collect(),
})
}
fn parse<B>(&self) -> Board<B>
where
B: Block,
{
let palette = self.get_palette();
Board::with_descriptions_and_palette(
self.parse_clues(&self.rows, &palette),
self.parse_clues(&self.columns, &palette),
Some(palette),
)
}
fn infer_scheme(&self) -> PuzzleScheme {
if !self.colors.is_empty() {
let mut names: Vec<_> = self.colors.values().map(|x| &x.name).collect();
names.sort_unstable();
if names != ["black", "white"] {
return PuzzleScheme::MultiColor;
}
}
PuzzleScheme::BlackAndWhite
}
}
impl OlsakParser {
fn parse_block<B>(&self, block: &str, palette: &ColorPalette) -> B
where
B: Block,
{
let mut as_chars = block.chars();
let value_color_pos = as_chars.position(|c| !c.is_digit(10));
#[allow(clippy::option_if_let_else)]
let (value, block_color) = if let Some(pos) = value_color_pos {
let (value, color) = block.split_at(pos);
(value, Some(color))
} else {
(block, None)
};
let color_name = block_color
.and_then(|block_color| self.colors.get(block_color))
.map(|color| &color.name);
let color_id = color_name.and_then(|name| palette.id_by_name(name));
B::from_str_and_color(value, color_id)
}
fn parse_line<B>(&self, descriptions: &[String], palette: &ColorPalette) -> Description<B>
where
B: Block,
{
Description::new(
descriptions
.iter()
.map(|block| self.parse_block(block, palette))
.collect(),
)
}
fn parse_clues<B>(
&self,
descriptions: &[Vec<String>],
palette: &ColorPalette,
) -> Vec<Description<B>>
where
B: Block,
{
descriptions
.iter()
.map(|line| self.parse_line(line, palette))
.collect()
}
}
impl Paletted for OlsakParser {
fn get_colors(&self) -> Vec<(String, char, String)> {
self.colors
.values()
.map(|x| (x.name.clone(), x.symbol, x.rgb.clone()))
.collect()
}
fn get_palette(&self) -> ColorPalette {
self.default_palette("white", "black")
}
}
#[derive(Debug)]
struct SimpleParser {
rows: Vec<Vec<String>>,
columns: Vec<Vec<String>>,
}
impl From<ParseIntError> for ParseError {
fn from(err: ParseIntError) -> Self {
Self(format!("{}", err))
}
}
impl SimpleParser {
fn parse_clues<B>(descriptions: &[Vec<String>]) -> Vec<Description<B>>
where
B: Block,
{
descriptions
.iter()
.map(|line| {
Description::new(
line.iter()
.filter_map(|block| {
let block = block.trim();
if block.is_empty() {
None
} else {
Some(B::from_str_and_color(block, None))
}
})
.collect(),
)
})
.collect()
}
fn split_into_blocks(lines: &[&str]) -> Vec<Vec<String>> {
lines
.iter()
.filter_map(|&line| {
if line.is_empty() {
None
} else {
Some(
line.split(&[' ', ','][..])
.map(ToString::to_string)
.collect(),
)
}
})
.collect()
}
fn remove_comments(text: &str) -> String {
let lines: Vec<_> = text
.lines()
.map(|line| {
if line.starts_with(&['#', '*'][..]) || line == "&" {
""
} else {
line.trim_end_matches(" 0")
}
})
.collect();
lines.join("\n").trim().to_string()
}
}
impl BoardParser for SimpleParser {
fn with_content(content: &str) -> Result<Self, ParseError>
where
Self: Sized,
{
let symbols: HashSet<_> = content
.lines()
.flat_map(|line| line.trim().chars())
.collect();
let solution_matrix_chars = ['0', '1'].iter().copied().collect();
if symbols == solution_matrix_chars {
let solution_matrix: Vec<_> = content
.lines()
.map(|line| {
line.chars()
.map(|ch| ch.to_digit(10).expect("not a decimal digit"))
.collect()
})
.collect();
let (columns, rows) = clues_from_solution(&solution_matrix, 0);
return Ok(Self {
rows: rows
.into_iter()
.map(|d| d.vec.iter().map(BinaryBlock::to_string).collect())
.collect(),
columns: columns
.into_iter()
.map(|d| d.vec.iter().map(BinaryBlock::to_string).collect())
.collect(),
});
}
let content = Self::remove_comments(content);
let (rows, columns) = {
let names = ["rows", "columns"];
let mut sections = split_sections(&content, &names, false, None);
if let Ok(sections) = sections.as_mut() {
(
sections.remove(names[0]).expect("Cannot find rows"),
sections.remove(names[1]).expect("Cannot find rows"),
)
} else {
let rows_section = "rows go first";
let columns_section = [""];
let mut sections =
split_sections(&content, &columns_section, true, Some(rows_section));
if let Ok(sections) = sections.as_mut() {
(
sections.remove(rows_section).expect("Cannot find rows"),
sections
.remove(columns_section[0])
.expect("Cannot find columns"),
)
} else {
let mut content_iter = content.lines();
let dimensions: Result<Vec<usize>, _> = content_iter
.next()
.expect("Empty content")
.split_whitespace()
.map(str::parse)
.collect();
let dimensions = dimensions?;
if dimensions.len() == 2 {
let (width, height) = (dimensions[0], dimensions[1]);
let rows = content_iter.by_ref().take(height).collect();
let columns = content_iter.take(width).collect();
(rows, columns)
} else {
unimplemented!("This puzzle format is not supported")
}
}
}
};
Ok(Self {
rows: Self::split_into_blocks(&rows),
columns: Self::split_into_blocks(&columns),
})
}
fn parse<B>(&self) -> Board<B>
where
B: Block,
{
let palette = self.get_palette();
Board::with_descriptions_and_palette(
Self::parse_clues(&self.rows),
Self::parse_clues(&self.columns),
Some(palette),
)
}
fn infer_scheme(&self) -> PuzzleScheme {
PuzzleScheme::BlackAndWhite
}
}
impl Paletted for SimpleParser {
fn get_colors(&self) -> Vec<(String, char, String)> {
vec![]
}
fn get_palette(&self) -> ColorPalette {
self.default_palette("white", "black")
}
}
#[cfg(test)]
#[cfg(feature = "ini")]
mod tests {
use crate::block::{base::color::ColorPalette, binary::BinaryBlock, Description};
use super::{BoardParser, MyFormat, Paletted, PuzzleScheme};
const fn block(n: usize) -> BinaryBlock {
BinaryBlock(n)
}
fn palette() -> ColorPalette {
ColorPalette::with_white_and_black("W", "B")
}
#[test]
fn parse_single() {
assert_eq!(
MyFormat::parse_clues(&String::from("1"), &palette()),
vec![Description::new(vec![block(1)])]
)
}
#[test]
fn parse_two_lines() {
assert_eq!(
MyFormat::parse_clues(&String::from("1\n2"), &palette()),
vec![
Description::new(vec![block(1)]),
Description::new(vec![block(2)])
]
)
}
#[test]
fn parse_two_rows_same_line() {
assert_eq!(
MyFormat::parse_clues(&String::from("1, 2"), &palette()),
vec![
Description::new(vec![block(1)]),
Description::new(vec![block(2)])
]
)
}
#[test]
fn parse_two_rows_with_commas() {
assert_eq!(
MyFormat::parse_clues(&String::from("1, 2,\n3"), &palette()),
vec![
Description::new(vec![block(1)]),
Description::new(vec![block(2)]),
Description::new(vec![block(3)]),
]
)
}
#[test]
fn parse_two_blocks() {
assert_eq!(
MyFormat::parse_clues(&String::from("1 2"), &palette()),
vec![Description::new(vec![block(1), block(2)]),]
)
}
#[test]
fn parse_quotes() {
assert_eq!(
MyFormat::parse_clues(&String::from("'1 2'"), &palette()),
vec![Description::new(vec![block(1), block(2)]),]
)
}
#[test]
fn parse_double_quotes() {
assert_eq!(
MyFormat::parse_clues(&String::from("1 2\n\"3 4\"\n"), &palette()),
vec![
Description::new(vec![block(1), block(2)]),
Description::new(vec![block(3), block(4)]),
]
)
}
#[test]
fn parse_comment_end_of_line() {
assert_eq!(
MyFormat::parse_clues(&String::from("1 2 # the comment"), &palette()),
vec![Description::new(vec![block(1), block(2)]),]
)
}
#[test]
fn parse_comment_semicolon() {
assert_eq!(
MyFormat::parse_clues(&String::from("1 2 ; another comment"), &palette()),
vec![Description::new(vec![block(1), block(2)]),]
)
}
#[test]
fn parse_comments_in_the_middle() {
assert_eq!(
MyFormat::parse_clues(
&String::from("1 2 \n # the multi-line \n # comment \n 3, 4"),
&palette(),
),
vec![
Description::new(vec![block(1), block(2)]),
Description::new(vec![block(3)]),
Description::new(vec![block(4)]),
]
)
}
#[test]
fn infer_black_and_white_no_colors_section() {
let s = r"
[clues]
rows = '1'
columns = '1'
";
assert_eq!(
MyFormat::with_content(s).unwrap().infer_scheme(),
PuzzleScheme::BlackAndWhite
)
}
#[test]
fn infer_black_and_white_empty_colors_section() {
let s = r"
[clues]
rows = '1'
columns = '1'
[colors]
";
assert_eq!(
MyFormat::with_content(s).unwrap().infer_scheme(),
PuzzleScheme::BlackAndWhite
)
}
#[test]
fn infer_black_and_white_empty_defs_in_colors_section() {
let s = r"
[clues]
rows = '1'
columns = '1'
[colors]
defs = []
";
assert_eq!(
MyFormat::with_content(s).unwrap().infer_scheme(),
PuzzleScheme::BlackAndWhite
)
}
#[test]
fn infer_multi_color() {
let s = r"
[clues]
rows = '1'
columns = '1'
[colors]
defs = ['g=(0, 204, 0) %']
";
assert_eq!(
MyFormat::with_content(s).unwrap().infer_scheme(),
PuzzleScheme::MultiColor
)
}
#[test]
fn parse_colors() {
let s = r"
[clues]
rows = '1'
columns = '1g'
[colors]
defs = ['g=(0, 204, 0) %']
";
let f = MyFormat::with_content(s).unwrap();
let colors = vec![("g".to_string(), '%', "0, 204, 0".to_string())];
assert_eq!(f.get_colors(), colors)
}
}