use std::{cmp::Ordering, collections::HashMap};
use edit_distance::edit_distance;
use itertools::Itertools;
use shortlist::Shortlist;
use crate::{
method::{class::FullClass, generate_title},
place_not::PnBlockParseError,
Method, PnBlock, Stage,
};
mod lib_serde;
pub(crate) mod parse_cc_lib;
type LibraryMap = HashMap<Stage, HashMap<String, CompactMethod>>;
#[derive(Debug, Clone)]
pub struct MethodLib {
method_map: LibraryMap,
}
impl MethodLib {
pub fn get_by_title(&self, title: &str) -> Result<Method, SearchError<()>> {
match self.get_by_title_option(&title.to_lowercase()) {
Some(Ok(method)) => Ok(method),
Some(Err((pn, error))) => Err(SearchError::PnParseErr { pn, error }),
None => Err(SearchError::NotFound(())),
}
}
fn get_by_title_option(
&self,
lower_case_title: &str,
) -> Option<Result<Method, (String, PnBlockParseError)>> {
let stage_name = lower_case_title.rsplit(' ').next().unwrap();
let stage = Stage::from_lower_case_name(stage_name)?;
let method = self
.method_map
.get(&stage)?
.get(lower_case_title)?
.to_method();
Some(method)
}
pub fn get_by_title_with_suggestions(
&self,
title: &str,
num_suggestions: usize,
) -> Result<Method, SearchError<Vec<(String, usize)>>> {
let lower_case_title = title.to_lowercase();
self.get_by_title(&lower_case_title).map_err(|e| {
e.map_not_found(|()| self.generate_suggestions(&lower_case_title, num_suggestions))
})
}
fn generate_suggestions(
&self,
lower_case_title: &str,
num_suggestions: usize,
) -> Vec<(String, usize)> {
#[derive(Debug, Clone)]
#[repr(transparent)]
struct Suggestion((String, usize));
impl Suggestion {
fn new(
actual_title: &str,
suggestion_title_lower: &str,
suggestion_title: String,
) -> Self {
Suggestion((
suggestion_title,
edit_distance(actual_title, suggestion_title_lower),
))
}
}
impl PartialOrd for Suggestion {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Suggestion {
fn cmp(&self, other: &Self) -> Ordering {
self.0 .1.cmp(&other.0 .1).reverse()
}
}
impl PartialEq for Suggestion {
fn eq(&self, other: &Self) -> bool {
self.0 .1 == other.0 .1
}
}
impl Eq for Suggestion {}
let mut suggestion_shortlist = Shortlist::new(num_suggestions);
for methods in self.method_map.values() {
suggestion_shortlist.append(methods.iter().map(|(stored_title, method)| {
Suggestion::new(lower_case_title, stored_title, method.title())
}));
}
let mut best_suggestions = suggestion_shortlist.into_sorted_vec();
best_suggestions.reverse();
best_suggestions
.into_iter()
.map(|Suggestion((title, edit_distance))| (title, edit_distance))
.collect_vec()
}
#[cfg(test)]
pub(crate) fn all_pns_and_classes(&self) -> Vec<(String, PnBlock, FullClass)> {
let mut v = Vec::new();
for (stage, meths) in &self.method_map {
for m in meths.values() {
v.push((
m.title(),
PnBlock::parse(&m.place_notation, *stage).unwrap(),
m.full_class,
));
}
}
v
}
}
#[cfg(feature = "method_lib_serde")]
impl MethodLib {
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string(&lib_serde::MethodLibSerde::from(self))
}
pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str::<lib_serde::MethodLibSerde>(json).map(Self::from)
}
}
#[cfg(feature = "cc_lib")]
use std::path::PathBuf;
#[cfg(feature = "cc_lib")]
impl MethodLib {
pub fn cc_lib() -> Option<MethodLib> {
if let Some(lib_from_cache) = Self::load_cc_lib_from_cache() {
return Some(lib_from_cache);
}
Self::fetch_cc_lib()
}
fn load_cc_lib_from_cache() -> Option<MethodLib> {
let cache_path = Self::cache_file_path()?;
let json = std::fs::read_to_string(cache_path).ok()?;
Self::from_json(&json).ok()
}
fn fetch_cc_lib() -> Option<MethodLib> {
let response = reqwest::blocking::get(
"https://raw.githubusercontent.com/kneasle/cc-method-lib/master/cccbr-methods.json",
)
.ok()?;
let json = response.text().ok()?;
let lib = Self::from_json(&json).ok()?;
if let Some(path) = Self::cache_file_path() {
let _ = std::fs::write(path, &json);
}
Some(lib)
}
fn cache_file_path() -> Option<PathBuf> {
let mut path = dirs::cache_dir()?;
path.push("cccbr-methods.json");
Some(path)
}
}
#[derive(Debug, Clone)]
struct CompactMethod {
name: String,
omit_class: bool,
full_class: FullClass,
place_notation: String,
stage: Stage,
}
impl CompactMethod {
fn to_method(&self) -> Result<Method, (String, PnBlockParseError)> {
Ok(Method::new(
self.name.to_owned(),
self.full_class,
self.omit_class,
PnBlock::parse(&self.place_notation, self.stage)
.map_err(|e| (self.place_notation.clone(), e))?
.to_block_from_rounds(),
))
}
fn title(&self) -> String {
generate_title(&self.name, self.full_class, self.omit_class, self.stage)
}
}
#[derive(Debug, Clone)]
pub enum SearchError<T> {
PnParseErr {
pn: String,
error: PnBlockParseError,
},
NotFound(T),
}
impl<T> SearchError<T> {
pub fn unwrap_parse_err(self) -> Result<Method, T> {
match self {
Self::PnParseErr { pn, error } => panic!("Error parsing {:?}: {}", pn, error),
Self::NotFound(v) => Err(v),
}
}
pub fn map_not_found<U>(self, f: impl FnOnce(T) -> U) -> SearchError<U> {
match self {
SearchError::PnParseErr { pn, error } => SearchError::PnParseErr { pn, error },
SearchError::NotFound(v) => SearchError::NotFound(f(v)),
}
}
}