ranting/lib.rs
1// (c) Roel Kluin 2022 GPL v3
2//!
3//! Functions to handle [Ranting](https://docs.rs/ranting_derive/0.2.1/ranting_derive/) trait placeholders.
4//!
5//! ## Feature flags
6#![doc = document_features::document_features!()]
7
8extern crate self as ranting;
9
10mod language;
11use lazy_static::lazy_static;
12use regex::Regex;
13
14#[doc(hidden)]
15pub use english_numbers::convert_no_fmt as rant_convert_numbers;
16
17// required for ranting_derive
18#[doc(hidden)]
19pub use strum_macros as rant_strum_macros;
20
21use in_definite::get_a_or_an;
22pub use language::english::inflect_possesive;
23use language::english::{
24 adapt_article, inflect_adjective, inflect_objective, inflect_subjective, inflect_verb,
25};
26pub use language::english_shared::{is_subject, is_subjective_plural};
27
28// TODO: make this a feature:
29//pub(crate) use strum_macros;
30
31/// A wrapper for `return Ok(say!())`
32///
33/// # Examples
34///
35/// ```rust
36/// # use ranting::{Noun, ack, Ranting};
37/// fn question(harr: Noun, friends: Noun, lad: Noun) -> Result<String, String> {
38/// ack!("{harr shall} {+=friends do} with {the drunken *lad}?");
39/// }
40///
41/// # fn main() {
42/// let harr = Noun::new("what", "it");
43/// let friends = Noun::new("crew", "we");
44/// let lad = Noun::new("sailor", "he");
45///
46/// assert_eq!(
47/// question(harr, friends, lad),
48/// Ok("What shall we do with the drunken sailor?".to_string())
49/// );
50/// # }
51/// ```
52pub use ranting_derive::ack;
53
54/// A wrapper for `return Err(say!())`
55///
56/// # Examples
57///
58/// ```rust
59/// # use ranting::{Noun, nay, Ranting};
60/// fn home(p: Noun) -> Result<String, String> {
61/// nay!("{=p can't} get in {`p} house.")
62/// }
63///
64/// # fn main() {
65/// assert_eq!(
66/// home(Noun::new("Jo", "she")),
67/// Err("She can't get in her house.".to_string())
68/// );
69/// # }
70/// ```
71pub use ranting_derive::nay;
72
73/// Functions like `format!()` for normal placeholders, but allows extended placeholders including
74/// e.g. articles or verbs beside a Noun or a variable with the Ranting trait. These are inflected
75/// accordingly, and adjustable by punctuation prefixes.
76///
77/// # Examples
78///
79/// ```rust
80/// # use ranting::{Noun, say, Ranting};
81/// fn inflect(with: Noun) -> String {
82/// let n = Noun::new("noun", "it");
83/// say!("{some n} with {0} {?n inflect} as {=0}, {@0}, {`0} and {~0}.", with)
84/// }
85///
86/// # fn main() {
87///
88/// assert_eq!(["I", "you", "he", "she", "it", "we", "they"]
89/// .iter()
90/// .map(|s| inflect(Noun::new(format!("subject {s}").as_str(), s)))
91/// .collect::<String>(),
92/// "A noun with subject I inflects as I, me, my and mine.\
93/// A noun with subject you inflects as you, you, your and yours.\
94/// A noun with subject he inflects as he, him, his and his.\
95/// A noun with subject she inflects as she, her, her and hers.\
96/// A noun with subject it inflects as it, it, its and its.\
97/// A noun with subject we inflects as we, us, our and ours.\
98/// A noun with subject they inflects as they, them, their and theirs."
99/// .to_string());
100/// # }
101/// ```
102pub use ranting_derive::say;
103
104/// If you want to implement Ranting on a `Box<&dyn Trait>` where Trait has Ranting
105pub use ranting_derive::boxed_ranting_trait;
106
107/// If you want to implement Ranting on a `&'_ dyn Trait` where Trait has Ranting
108pub use ranting_derive::ref_ranting_trait;
109
110fn get_article_or_so<R>(noun: &R, s: &str, space: &str, as_pl: bool, uc: bool) -> Option<String>
111where
112 R: Ranting,
113{
114 if noun.skip_article() && !s.starts_with('!') && !matches!(s, "these" | "those") {
115 return Some("".to_string());
116 }
117 match s.trim_start_matches('!') {
118 "the" => Some(uc_1st_if(s, uc)),
119 "a" | "an" | "some" => {
120 let singular = noun.inflect(false, false);
121 let a_or_an = uc_1st_if(get_a_or_an(&singular), uc);
122 Some(ranting::adapt_article(&a_or_an, s, space, as_pl, uc))
123 }
124 "these" | "those" => Some(ranting::adapt_article(s, s, space, as_pl, uc)),
125 _ => None,
126 }
127}
128
129/// The say macro parses placeholders and passes captures to this function which returns a string.
130pub fn handle_placeholder<R>(
131 noun: &R,
132 poss: String,
133 nr: String,
134 mut uc: bool,
135 caps: [&str; 5],
136) -> String
137where
138 R: Ranting,
139{
140 lazy_static! {
141 static ref OF: Regex = Regex::new(r"\bof\s+$").unwrap();
142 }
143 let [mut pre, plurality, noun_space, case, mut post] = caps;
144 let has_possesive = pre.contains('`');
145 let singular_post_verb = OF.is_match(pre); // e.g. "{a set of $ten are} still singular"
146
147 let as_pl = match plurality {
148 "" => noun.is_plural(),
149 "+" => true,
150 "-" => false,
151 // A bit hackish but should work also for e.g. 1.0%
152 "#" => nr.trim_start() != "one",
153 _ => {
154 let s = nr.trim_start();
155 s != "1" && s.split('.').next() != Some("1")
156 }
157 };
158 let pre_string = pre.replace('`', poss.as_str());
159
160 let mut space;
161 (pre, space) = split_at_find_end(&pre_string, |c: char| !c.is_whitespace())
162 .unwrap_or((pre_string.as_str(), ""));
163
164 let mut etc1;
165 (pre, etc1) = split_at_find_start(pre, |c| c.is_whitespace()).unwrap_or((pre, ""));
166
167 let subjective = noun.subjective();
168 let mut res = String::new();
169
170 // This may be an article or certain verbs that can occur before the noun:
171 if !pre.is_empty() {
172 let p = pre.to_lowercase();
173 if let Some(a) = get_article_or_so(noun, p.as_str(), space, as_pl, uc) {
174 res.push_str(&a);
175 } else if has_possesive {
176 res.push_str(&uc_1st_if(pre, uc));
177 } else {
178 assert!(post.is_empty(), "verb before and after?");
179 let verb = inflect_verb(subjective, p.as_str(), as_pl, uc);
180 res.push_str(&verb);
181 if !etc1.is_empty() {
182 let art_space;
183 (art_space, etc1) =
184 split_at_find_start(etc1, |c| !c.is_whitespace()).unwrap_or(("", etc1));
185 res.push_str(art_space);
186 let s;
187 (s, etc1) = split_at_find_start(etc1, |c| c.is_whitespace()).unwrap_or((etc1, ""));
188 if let Some(a) = get_article_or_so(noun, s, space, as_pl, false) {
189 res.push_str(&a);
190 } else {
191 res.push_str(s);
192 }
193 }
194 }
195 res.push_str(etc1);
196 res.push_str(space);
197 uc = false;
198 }
199 if !plurality.contains('?') {
200 res.push_str(&nr);
201 }
202
203 (space, post) = split_at_find_start(post, |c: char| !c.is_whitespace()).unwrap_or(("", post));
204
205 if case != "?" {
206 res.push_str(noun_space);
207 let s = match case {
208 "=" => inflect_subjective(subjective, as_pl, uc),
209 "@" => inflect_objective(subjective, as_pl, uc),
210 "`" => inflect_possesive(subjective, as_pl, uc),
211 "~" => inflect_adjective(subjective, as_pl, uc),
212 _ => noun.inflect(as_pl, uc),
213 };
214 res.push_str(&s);
215 res.push_str(space);
216 uc = false;
217 }
218 let etc2;
219 (etc2, post) = split_at_find_end(post, |c| c.is_whitespace()).unwrap_or(("", post));
220
221 res.push_str(etc2);
222 if !post.is_empty() {
223 match post {
224 "'" | "'s" => {
225 res.push_str(adapt_possesive_s(noun, as_pl));
226 }
227 v => {
228 let verb = inflect_verb(subjective, v, !singular_post_verb && as_pl, uc);
229 res.push_str(&verb);
230 }
231 }
232 }
233 res
234}
235
236/// upper cases first character if uc is true, or second in a contraction.
237pub fn uc_1st_if(s: &str, uc: bool) -> String {
238 if uc {
239 let mut c = s.chars();
240 c.next()
241 .map(|t| match t {
242 '\'' => {
243 t.to_string()
244 + &c.next()
245 .map(|c| c.to_uppercase().collect::<String>())
246 .unwrap_or_default()
247 }
248 _ => t.to_uppercase().collect::<String>(),
249 })
250 .unwrap_or_default()
251 + c.as_str()
252 } else {
253 s.to_string()
254 }
255}
256
257fn split_at_find_start(s: &str, fun: fn(char) -> bool) -> Option<(&str, &str)> {
258 s.find(fun).map(|u| s.split_at(u))
259}
260
261fn split_at_find_end(s: &str, fun: fn(char) -> bool) -> Option<(&str, &str)> {
262 s.rfind(fun).map(|u| s.split_at(u + 1))
263}
264
265/// Has the Ranting trait. Often you may want to `#[derive(Ranting)]` and sometimes override some
266/// of the trait functions.
267#[derive(ranting_derive::Ranting)]
268// By setting name and subject to "$", these must come from the struct.
269#[ranting(name = "$", subject = "$")]
270pub struct Noun {
271 name: String,
272 subject: String,
273}
274impl Noun {
275 pub fn new(name: &str, subject: &str) -> Self {
276 assert!(is_subject(subject), "not a subject");
277 Noun {
278 name: name.to_string(),
279 subject: subject.to_string(),
280 }
281 }
282}
283
284/// convert to `'s` or `'` as appropriate for singular or plural of a noun.
285///
286/// # Examples
287///
288/// ```rust
289/// # use ranting::*;
290/// # fn main() {
291///
292/// let school = Noun::new("school", "it");
293/// let principal = Noun::new("principal", "she");
294/// let myles = Noun::new("Myles", "he");
295///
296/// assert_eq!(say!("{the school'} {principal are} also {myles'}, but only one of all {the +school's} {+principal} in town."),
297/// "The school's principal is also Myles's, but only one of all the schools' principals in town.".to_string());
298/// # }
299/// ```
300// a combined plural may require some tricks: "The star and cross' design was pattented by Bob."
301fn adapt_possesive_s(noun: &dyn Ranting, asked_plural: bool) -> &str {
302 if asked_plural && !is_name(noun) {
303 "'"
304 } else {
305 "'s"
306 }
307}
308fn is_name(noun: &dyn Ranting) -> bool {
309 noun.name(false)
310 .trim_start_matches('\'')
311 .starts_with(|c: char| c.is_uppercase())
312}
313
314/// The trait required for a struct or enum to function as a noun in a placeholder, derived with `#[derive_ranting]`.
315/// Functions are used in `say!()` placeholders replacements.
316///
317/// # Examples
318///
319/// ```
320/// # use std::str::FromStr;
321/// # use ranting::*;
322/// # use ranting_derive::*;
323/// #[derive_ranting]
324/// #[ranting(subject = "you", plural_you = true)]
325/// struct OpponentTeam {}
326///
327/// #[derive_ranting]
328/// #[ranting(subject = "he")]
329/// struct ChessPlayer {}
330///
331/// fn big_words_to<T: Ranting>(who: T) -> String {
332/// say!("I will grant {@0} {`0} fight, but {=0 are} going to lose today.", who)
333/// }
334///
335/// # fn main() {
336/// let team = OpponentTeam {};
337/// assert_eq!(big_words_to(team),
338/// "I will grant you your fight, but you are going to lose today.");
339///
340/// let magnus = ChessPlayer {};
341/// assert_eq!(big_words_to(magnus),
342/// "I will grant him his fight, but he is going to lose today.");
343/// # }
344/// ```
345// By overriding functions one can adapt default behavior, which affects the
346// [placeholder](https://docs.rs/ranting_derive/0.2.1/ranting_derive/) behavior.
347pub trait Ranting: std::fmt::Display {
348 /// return the name, which is struct name or the `#{ranting(name = "..")]` value, or self.name
349 /// if the name attribute was set to "$"
350 fn name(&self, uc: bool) -> String;
351 /// return the subject: "it" or the `#{ranting(subject = "..")]` value; self.subject if "$".
352 fn subjective(&self) -> &str;
353 /// return if plural (the subject, or if you, the `#{ranting(plural_you = "true/false")]` value,
354 /// default false
355 // if the subject can be "you" in both forms, you may want to override the function.
356 fn is_plural(&self) -> bool;
357 /// return the singular or plural form as configured, starting with capital if uc is set.
358 /// use `#{ranting(singular_end = "..", plural_end = "..")]` if not plural = singular + "s"
359 // if name can change this should be overridden to lookup each singular_end and plural_end:
360 fn inflect(&self, to_plural: bool, uc: bool) -> String;
361 /// If an article is only required when emphasizing, set `#{ranting(no_article = "true")]`,
362 /// and this function will return accordingly (used by placeholders).
363 // examples: Names, languages, elements, food grains, meals (unless particular), sports.
364 // if name can change and sometimes goes without article (e.g. a sport) lookup & override:
365 fn skip_article(&self) -> bool;
366}