From 26eb35cced6a9bb980786db741c663a74bda0332 Mon Sep 17 00:00:00 2001 From: dmyTRUEk <25669613+dmyTRUEk@users.noreply.github.com> Date: Fri, 29 Mar 2024 20:25:13 +0200 Subject: [PATCH] feat: format option --- src/format.rs | 485 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 42 +++-- 2 files changed, 517 insertions(+), 10 deletions(-) create mode 100644 src/format.rs diff --git a/src/format.rs b/src/format.rs new file mode 100644 index 0000000..1453f52 --- /dev/null +++ b/src/format.rs @@ -0,0 +1,485 @@ +//! Format quote by %q and other. + +use crate::{quote::Quote, to_str::ToStr}; + + +pub const FORMATTING_DEFAULT: &str = "\"%q\"\n-- %c( to %t)( about %a), \"%s\""; + +const FORMAT_CHAR_TEXT: char = 'q'; +const FORMAT_CHAR_CHAR: char = 'c'; +const FORMAT_CHAR_SRC : char = 's'; +const FORMAT_CHAR_WHOM_TO: char = 't'; +const FORMAT_CHAR_WHOM_ABOUT: char = 'a'; +const BRACKET_LEFT: char = '('; +const BRACKET_RIGHT: char = ')'; + + +pub fn format_quote(quote: &Quote, format: &str) -> String { + let Quote { text, char, src, whom_to, whom_about } = quote; + + let mut quote_formatted: String = format + .replace(&get_formatter(FORMAT_CHAR_TEXT), text) + .replace(&get_formatter(FORMAT_CHAR_CHAR), char.to_str()) + .replace(&get_formatter(FORMAT_CHAR_SRC), src); + + quote_formatted = format_optional( + "e_formatted, + FORMAT_CHAR_WHOM_TO, + whom_to.as_ref().map(|whom_to| whom_to.to_str().to_string()), + ); + + quote_formatted = format_optional( + "e_formatted, + FORMAT_CHAR_WHOM_ABOUT, + whom_about.as_ref().map(|whom_about| whom_about.to_str().to_string()), + ); + + quote_formatted = quote_formatted.replace("\\n", "\n"); + + quote_formatted +} + + +fn get_formatter(c: char) -> String { + format!("%{c}") +} + +fn format_optional( + quote_formatted: &str, + format_char: char, + replace_formatter_by: Option, +) -> String { + let maybe_amp_pos: Option = quote_formatted + .find(&get_formatter(format_char)); + let amp_pos: usize = match maybe_amp_pos { + None => return quote_formatted.to_string(), + Some(amp_pos) => amp_pos, + }; + + let maybe_l_bracket_pos: Option = quote_formatted[..amp_pos] + .char_indices() + .rev() + .find(|(_i, c)| *c == BRACKET_LEFT) + .map(|(i, _c)| i); + + let maybe_r_bracket_pos: Option = quote_formatted[amp_pos..] + .char_indices() + .find(|(_i, c)| *c == BRACKET_RIGHT) + .map(|(i, _c)| i) + .map(|i| i + amp_pos); + + match (maybe_l_bracket_pos, maybe_r_bracket_pos) { + (Some(l_bracket_pos), Some(r_bracket_pos)) => { + let replace_formatting_by: String = match replace_formatter_by { + Some(replace_formatter_by) => { + quote_formatted[l_bracket_pos+1..=r_bracket_pos-1] + .replace( + &get_formatter(format_char), + &replace_formatter_by + ) + } + None => String::new(), + }; + [ + "e_formatted[..l_bracket_pos], + &replace_formatting_by, + "e_formatted[r_bracket_pos+1..], + ].concat() + } + _ => quote_formatted.to_string() + } +} + + + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn default_without_to_without_about() { + use crate::characters::Character::Marisa_Kirisame; + let quote = Quote { + text: "It ain't magic if it ain't flashy. Danmaku's all about firepower.", + char: Marisa_Kirisame, + src: "Perfect Memento in Strict Sense", + ..Quote::default() + }; + assert_eq!( + String::from("\ + \"It ain't magic if it ain't flashy. Danmaku's all about firepower.\"\n\ + -- Marisa Kirisame, \"Perfect Memento in Strict Sense\"\ + "), + format_quote("e, FORMATTING_DEFAULT), + ); + } + + #[test] + fn default_with_to_without_about() { + use crate::characters::Character::{Shinki, Yuuka_Kazami}; + let quote = Quote { + text: "Massacres are a kind of game, too. It doesn't matter whether it's humans or Makai residents", + char: Yuuka_Kazami, + src: "Mystic Square", + whom_to: Some(Shinki), + ..Quote::default() + }; + assert_eq!( + String::from("\ + \"Massacres are a kind of game, too. It doesn't matter whether it's humans or Makai residents\"\n\ + -- Yuuka Kazami to Shinki, \"Mystic Square\"\ + "), + format_quote("e, FORMATTING_DEFAULT), + ); + } + + #[test] + fn default_without_to_with_about() { + use crate::characters::Character::{Hieda_no_Akyuu, Reimu_Hakurei}; + let quote = Quote { + text: "Out of the generations of shrine maidens, her sense of danger is the most lacking and she has meager training, yet her power is considerable.", + char: Hieda_no_Akyuu, + src: "Perfect Memento in Strict Sense", + whom_about: Some(Reimu_Hakurei), + ..Quote::default() + }; + assert_eq!( + String::from("\ + \"Out of the generations of shrine maidens, her sense of danger is the most lacking and she has meager training, yet her power is considerable.\"\n\ + -- Hieda no Akyuu about Reimu Hakurei, \"Perfect Memento in Strict Sense\"\ + "), + format_quote("e, FORMATTING_DEFAULT), + ); + } + + #[test] + fn default_with_to_with_about() { + use crate::characters::Character::{Shinki, Yumeko, Yuuka_Kazami}; + let quote = Quote { + text: "You musn't dirty your hands dealing with this kind of person! I shall deal with her promptly, so please, step back, Lady Shinki.", + char: Yumeko, + src: "Mystic Square, Stage 5", + whom_to: Some(Shinki), + whom_about: Some(Yuuka_Kazami), + }; + assert_eq!( + String::from("\ + \"You musn't dirty your hands dealing with this kind of person! I shall deal with her promptly, so please, step back, Lady Shinki.\"\n\ + -- Yumeko to Shinki about Yuuka Kazami, \"Mystic Square, Stage 5\"\ + "), + format_quote("e, FORMATTING_DEFAULT), + ); + } + + mod optional { + use super::*; + use crate::characters::Character::Cirno; + + mod single_byte { + use super::*; + + mod none { + use super::*; + const QUOTE: Quote = Quote { + whom_to: None, + ..Quote::default() + }; + + #[test] + fn only_formatter() { + assert_eq!( + String::from(""), + format_quote("E, "(%t)"), + ); + } + + #[test] + fn text_outside_on_left() { + assert_eq!( + String::from("abc"), + format_quote("E, "abc(%t)"), + ); + } + + #[test] + fn text_outside_on_right() { + assert_eq!( + String::from("abc"), + format_quote("E, "(%t)abc"), + ); + } + + #[test] + fn surrounded_outside() { + assert_eq!( + String::from("abcdef"), + format_quote("E, "abc(%t)def"), + ); + } + + #[test] + fn text_inside_on_left() { + assert_eq!( + String::from(""), + format_quote("E, "(abc%t)"), + ); + } + + #[test] + fn text_inside_on_right() { + assert_eq!( + String::from(""), + format_quote("E, "(%tabc)"), + ); + } + + #[test] + fn surrounded_inside() { + assert_eq!( + String::from(""), + format_quote("E, "(abc%tdef)"), + ); + } + + #[test] + fn surrounded() { + assert_eq!( + String::from("abcjkl"), + format_quote("E, "abc(def%tghi)jkl"), + ); + } + } + + mod some { + use super::*; + const QUOTE: Quote = Quote { + whom_to: Some(Cirno), + ..Quote::default() + }; + + #[test] + fn only_formatter() { + assert_eq!( + String::from("Cirno"), + format_quote("E, "(%t)"), + ); + } + + #[test] + fn text_outside_on_left() { + assert_eq!( + String::from("abcCirno"), + format_quote("E, "abc(%t)"), + ); + } + + #[test] + fn text_outside_on_right() { + assert_eq!( + String::from("Cirnoabc"), + format_quote("E, "(%t)abc"), + ); + } + + #[test] + fn surrounded_outside() { + assert_eq!( + String::from("abcCirnodef"), + format_quote("E, "abc(%t)def"), + ); + } + + #[test] + fn text_inside_on_left() { + assert_eq!( + String::from("abcCirno"), + format_quote("E, "(abc%t)"), + ); + } + + #[test] + fn text_inside_on_right() { + assert_eq!( + String::from("Cirnoabc"), + format_quote("E, "(%tabc)"), + ); + } + + #[test] + fn surrounded_inside() { + assert_eq!( + String::from("abcCirnodef"), + format_quote("E, "(abc%tdef)"), + ); + } + + #[test] + fn surrounded() { + assert_eq!( + String::from("abcdefCirnoghijkl"), + format_quote("E, "abc(def%tghi)jkl"), + ); + } + } + } + + mod multi_byte { + use super::*; + + mod none { + use super::*; + const QUOTE: Quote = Quote { + whom_to: None, + ..Quote::default() + }; + + #[test] + fn only_formatter() { + assert_eq!( + String::from(""), + format_quote("E, "(%t)"), + ); + } + + #[test] + fn text_outside_on_left() { + assert_eq!( + String::from("東"), + format_quote("E, "東(%t)"), + ); + } + + #[test] + fn text_outside_on_right() { + assert_eq!( + String::from("東"), + format_quote("E, "(%t)東"), + ); + } + + #[test] + fn surrounded_outside() { + assert_eq!( + String::from("東方"), + format_quote("E, "東(%t)方"), + ); + } + + #[test] + fn text_inside_on_left() { + assert_eq!( + String::from(""), + format_quote("E, "(東%t)"), + ); + } + + #[test] + fn text_inside_on_right() { + assert_eq!( + String::from(""), + format_quote("E, "(%t東)"), + ); + } + + #[test] + fn surrounded_inside() { + assert_eq!( + String::from(""), + format_quote("E, "(東%t方)"), + ); + } + + #[test] + fn surrounded() { + assert_eq!( + String::from("東石"), + format_quote("E, "東(方%t小)石"), + ); + } + } + + mod some { + use super::*; + const QUOTE: Quote = Quote { + whom_to: Some(Cirno), + ..Quote::default() + }; + + #[test] + fn only_formatter() { + assert_eq!( + String::from("Cirno"), + format_quote("E, "(%t)"), + ); + } + + #[test] + fn text_outside_on_left() { + assert_eq!( + String::from("東Cirno"), + format_quote("E, "東(%t)"), + ); + } + + #[test] + fn text_outside_on_right() { + assert_eq!( + String::from("Cirno東"), + format_quote("E, "(%t)東"), + ); + } + + #[test] + fn surrounded_outside() { + assert_eq!( + String::from("東Cirno方"), + format_quote("E, "東(%t)方"), + ); + } + + #[test] + fn text_inside_on_left() { + assert_eq!( + String::from("東Cirno"), + format_quote("E, "(東%t)"), + ); + } + + #[test] + fn text_inside_on_right() { + assert_eq!( + String::from("Cirno東"), + format_quote("E, "(%t東)"), + ); + } + + #[test] + fn surrounded_inside() { + assert_eq!( + String::from("東Cirno方"), + format_quote("E, "(東%t方)"), + ); + } + + #[test] + fn surrounded() { + assert_eq!( + String::from("東方Cirno小石"), + format_quote("E, "東(方%t小)石"), + ); + } + } + } + + #[ignore] + #[test] + fn escaped_bracket_left() { + todo!() + } + #[ignore] + #[test] + fn escaped_bracket_right() { + todo!() + } + } +} diff --git a/src/main.rs b/src/main.rs index 7727943..7c1d443 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,12 +6,19 @@ use rand::{thread_rng, Rng}; // mod artbooks; mod characters; // mod games; +mod format; mod quote; mod quotes; // mod source; mod to_str; -use crate::{quote::Quote, quotes::QUOTES, to_str::ToStr}; +use crate::{ + format::{FORMATTING_DEFAULT, format_quote}, + quote::Quote, + quotes::QUOTES, + to_str::ToStr, +}; + #[derive(Parser, Debug)] #[command(about, version, long_about = None, author)] @@ -19,19 +26,34 @@ struct CliArgs { /// Name of the character #[arg(short, long)] character: Option, + + /// Format string + /// + /// Formatters: + /// %q - quote (text) + /// %c - character + /// %s - source + /// (%t) - whom to + /// (%a) - whom about + /// + /// Optional formatter must be enclosed in round brackets, + /// which may have additional text inside. + /// + /// Example: + /// `"%q" -- %c( says to %t), %s` + /// if %t is none: + /// `"My hat is my friend." -- Koishi Komeiji, KKHTA` + /// if %t is not none: + /// `"My hat is my friend." -- Koishi Komeiji says to Koishi Komeiji, KKHTA` + #[arg(short, long, verbatim_doc_comment, default_value_t={FORMATTING_DEFAULT.to_string()})] + format: String, } fn main() -> Result<(), &'static str> { let cli_args = CliArgs::parse(); - let Quote { text, char, src, whom_to, whom_about } = get_random_quote(cli_args.character)?; - let char = char.to_str(); - let maybe_to = whom_to - .map(|whom_to| format!(" to {}", whom_to.to_str())) - .unwrap_or_default(); - let maybe_about = whom_about - .map(|whom_about| format!(" about {}", whom_about.to_str())) - .unwrap_or_default(); - println!("\"{text}\"\n-- {char}{maybe_to}{maybe_about}, \"{src}\""); + let quote = get_random_quote(cli_args.character)?; + let quote_formatted = format_quote(quote, &cli_args.format); + println!("{quote_formatted}"); Ok(()) }