1#[cfg(not(feature = "std"))]
2use alloc::format;
3#[cfg(not(feature = "std"))]
4use alloc::string::{String, ToString};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
12#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
13pub enum Tense {
14 #[default]
15 Past,
16 Present,
17 Future,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
22#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
23pub enum Aspect {
24 #[default]
26 Simple,
27 Perfect,
29 Progressive,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
35#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
36pub enum Voice {
37 Active,
39 #[default]
41 Passive,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
46#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
47pub enum Person {
48 First,
49 Second,
50 #[default]
51 Third,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
56#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
57pub enum Mood {
58 #[default]
59 Indicative,
60 Conditional,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
68pub enum Conjunction {
69 And,
70 Or,
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
82pub enum VerbForm {
83 SimplePast,
85 SimplePresent,
87 SimpleFuture,
89 PresentPerfect,
91 PastPerfect,
93 FuturePerfect,
95 PresentProgressive,
97 PastProgressive,
99 Conditional,
101 ConditionalPerfect,
103}
104
105impl VerbForm {
106 pub fn resolve(self) -> (Tense, Aspect, Mood) {
108 use Aspect::*;
109 use Mood::*;
110 use Tense::*;
111 match self {
112 VerbForm::SimplePast => (Past, Simple, Indicative),
113 VerbForm::SimplePresent => (Present, Simple, Indicative),
114 VerbForm::SimpleFuture => (Future, Simple, Indicative),
115 VerbForm::PresentPerfect => (Present, Perfect, Indicative),
116 VerbForm::PastPerfect => (Past, Perfect, Indicative),
117 VerbForm::FuturePerfect => (Future, Perfect, Indicative),
118 VerbForm::PresentProgressive => (Present, Progressive, Indicative),
119 VerbForm::PastProgressive => (Past, Progressive, Indicative),
120 VerbForm::Conditional => (Present, Simple, Conditional),
122 VerbForm::ConditionalPerfect => (Present, Perfect, Conditional),
123 }
124 }
125
126 pub fn parse_spec(spec: &str) -> Option<(VerbForm, Voice)> {
130 let (voice, rest) = if let Some(tail) = spec.strip_prefix("active_") {
131 (Voice::Active, tail)
132 } else if let Some(tail) = spec.strip_prefix("passive_") {
133 (Voice::Passive, tail)
134 } else {
135 (Voice::Passive, spec)
136 };
137
138 let form = match rest {
139 "past" | "simple_past" => VerbForm::SimplePast,
140 "present" | "simple_present" => VerbForm::SimplePresent,
141 "future" | "simple_future" => VerbForm::SimpleFuture,
142 "present_perfect" => VerbForm::PresentPerfect,
143 "past_perfect" => VerbForm::PastPerfect,
144 "future_perfect" => VerbForm::FuturePerfect,
145 "present_progressive" | "progressive" => VerbForm::PresentProgressive,
146 "past_progressive" => VerbForm::PastProgressive,
147 "conditional" => VerbForm::Conditional,
148 "conditional_perfect" => VerbForm::ConditionalPerfect,
149 _ => return None,
150 };
151
152 Some((form, voice))
153 }
154}
155
156impl From<Tense> for VerbForm {
157 fn from(tense: Tense) -> Self {
158 match tense {
159 Tense::Past => VerbForm::SimplePast,
160 Tense::Present => VerbForm::SimplePresent,
161 Tense::Future => VerbForm::SimpleFuture,
162 }
163 }
164}
165
166#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
179#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
180pub enum PluralCategory {
181 Zero,
182 One,
183 Two,
184 Few,
185 Many,
186 #[default]
189 Other,
190}
191
192pub trait Language: Send + Sync {
197 fn pluralize(&self, word: &str, count: usize) -> String;
200
201 fn singularize(&self, word: &str) -> String;
203
204 fn article(&self, word: &str) -> &str;
206
207 fn conjugate(&self, verb: &str, tense: Tense, person: Person) -> String;
210
211 fn past_participle(&self, verb: &str) -> String;
215
216 fn present_participle(&self, verb: &str) -> String;
219
220 fn join_list(&self, items: &[&str], conjunction: Conjunction) -> String;
223
224 fn ordinal(&self, n: usize) -> String;
226
227 fn number_to_words(&self, n: usize) -> String;
229
230 fn plural_description(
250 &self,
251 entity_type: &str,
252 count: usize,
253 _features: &crate::agreement::AgreementFeatures,
254 ) -> String {
255 match count {
256 0 => String::new(),
257 1 => format!("the {entity_type}"),
258 _ => format!("the {count} {}", self.pluralize(entity_type, count)),
259 }
260 }
261
262 fn verb_phrase(&self, verb: &str, form: VerbForm, voice: Voice, person: Person) -> String {
270 english_verb_phrase(self, verb, form, voice, person)
271 }
272
273 fn plural_category(&self, n: i64) -> PluralCategory {
281 match n {
282 1 => PluralCategory::One,
283 _ => PluralCategory::Other,
284 }
285 }
286
287 fn pluralize_with_category(&self, word: &str, category: PluralCategory) -> String {
299 match category {
300 PluralCategory::One => word.to_string(),
301 _ => self.pluralize(word, 2),
302 }
303 }
304
305 fn is_connective_opener(&self, text: &str) -> bool {
326 const ENGLISH_OPENERS: &[&str] = &[
327 "Additionally,",
329 "Furthermore,",
330 "It also",
331 "Similarly,",
333 "Likewise,",
334 "Meanwhile,",
336 "However,",
337 "On the other hand,",
338 "Because of this,",
340 "As a result,",
341 "Nevertheless,",
342 "Then,",
343 "If this happens,",
344 "In summary,",
345 ];
346 ENGLISH_OPENERS
347 .iter()
348 .any(|opener| text.starts_with(opener))
349 }
350
351 fn discourse_marker(&self, relation: crate::rst::RstRelation) -> Option<&'static str> {
352 use crate::rst::RstRelation::*;
353 Some(match relation {
354 Elaboration => "Furthermore, ",
355 Contrast => "However, ",
356 Cause => "Because of this, ",
357 Result => "As a result, ",
358 Concession => "Nevertheless, ",
359 Sequence => "Then, ",
360 Condition => "If this happens, ",
361 Background => "Meanwhile, ",
362 Summary => "In summary, ",
363 })
364 }
365
366 fn proportion_phrase(
382 &self,
383 matching: i64,
384 total: i64,
385 noun_singular: Option<&str>,
386 _features: &crate::agreement::AgreementFeatures,
387 ) -> String {
388 crate::proportion::english_proportion(self, matching, total, noun_singular)
389 }
390
391 #[cfg(feature = "time")]
402 fn since_last_marker(&self, diff_secs: i64) -> String {
403 crate::time::format_since_last(diff_secs)
404 }
405
406 fn realize_reference(
432 &self,
433 form: crate::discourse::ReferenceForm,
434 features: &crate::agreement::AgreementFeatures,
435 ) -> Option<String> {
436 use crate::agreement::Number;
437 use crate::discourse::ReferenceForm;
438 match form {
439 ReferenceForm::Pronoun => Some(match features.number {
440 Number::Plural | Number::Dual => "they".to_string(),
441 _ => "it".to_string(),
442 }),
443 ReferenceForm::Possessive => Some(match features.number {
444 Number::Plural | Number::Dual => "their".to_string(),
445 _ => "its".to_string(),
446 }),
447 ReferenceForm::Demonstrative => Some("this".to_string()),
448 ReferenceForm::Zero => None,
449 ReferenceForm::Full | ReferenceForm::ShortName => None,
450 }
451 }
452
453 fn possessive_name(&self, owner: &str) -> String {
461 let owner = owner.trim();
462 if owner.is_empty() {
463 return String::new();
464 }
465 if owner.ends_with('s') || owner.ends_with('S') {
466 format!("{owner}'")
467 } else {
468 format!("{owner}'s")
469 }
470 }
471}
472
473pub fn english_verb_phrase<L: Language + ?Sized>(
477 lang: &L,
478 verb: &str,
479 form: VerbForm,
480 voice: Voice,
481 person: Person,
482) -> String {
483 let (tense, aspect, mood) = form.resolve();
484 let past_participle = lang.past_participle(verb);
485 let present_participle = lang.present_participle(verb);
486
487 let have_aux = lang.conjugate("have", Tense::Present, person);
490 let had_aux = "had";
491 let be_present = lang.conjugate("be", Tense::Present, person);
492 let be_past = match person {
493 Person::Third | Person::First => "was".to_string(),
494 Person::Second => "were".to_string(),
495 };
496
497 match mood {
498 Mood::Conditional => match (aspect, voice) {
499 (Aspect::Simple, Voice::Active) => format!("would {verb}"),
500 (Aspect::Simple, Voice::Passive) => format!("would be {past_participle}"),
501 (Aspect::Perfect, Voice::Active) => format!("would have {past_participle}"),
502 (Aspect::Perfect, Voice::Passive) => {
503 format!("would have been {past_participle}")
504 }
505 (Aspect::Progressive, Voice::Active) => format!("would be {present_participle}"),
506 (Aspect::Progressive, Voice::Passive) => {
507 format!("would be being {past_participle}")
508 }
509 },
510 Mood::Indicative => match (tense, aspect, voice) {
511 (Tense::Past, Aspect::Simple, Voice::Active) => {
513 lang.conjugate(verb, Tense::Past, person)
514 }
515 (Tense::Past, Aspect::Simple, Voice::Passive) => {
516 format!("{be_past} {past_participle}")
517 }
518 (Tense::Present, Aspect::Simple, Voice::Active) => {
519 lang.conjugate(verb, Tense::Present, person)
520 }
521 (Tense::Present, Aspect::Simple, Voice::Passive) => {
522 format!("{be_present} {past_participle}")
523 }
524 (Tense::Future, Aspect::Simple, Voice::Active) => format!("will {verb}"),
525 (Tense::Future, Aspect::Simple, Voice::Passive) => {
526 format!("will be {past_participle}")
527 }
528
529 (Tense::Past, Aspect::Perfect, Voice::Active) => {
531 format!("{had_aux} {past_participle}")
532 }
533 (Tense::Past, Aspect::Perfect, Voice::Passive) => {
534 format!("{had_aux} been {past_participle}")
535 }
536 (Tense::Present, Aspect::Perfect, Voice::Active) => {
537 format!("{have_aux} {past_participle}")
538 }
539 (Tense::Present, Aspect::Perfect, Voice::Passive) => {
540 format!("{have_aux} been {past_participle}")
541 }
542 (Tense::Future, Aspect::Perfect, Voice::Active) => {
543 format!("will have {past_participle}")
544 }
545 (Tense::Future, Aspect::Perfect, Voice::Passive) => {
546 format!("will have been {past_participle}")
547 }
548
549 (Tense::Past, Aspect::Progressive, Voice::Active) => {
551 format!("{be_past} {present_participle}")
552 }
553 (Tense::Past, Aspect::Progressive, Voice::Passive) => {
554 format!("{be_past} being {past_participle}")
555 }
556 (Tense::Present, Aspect::Progressive, Voice::Active) => {
557 format!("{be_present} {present_participle}")
558 }
559 (Tense::Present, Aspect::Progressive, Voice::Passive) => {
560 format!("{be_present} being {past_participle}")
561 }
562 (Tense::Future, Aspect::Progressive, Voice::Active) => {
563 format!("will be {present_participle}")
564 }
565 (Tense::Future, Aspect::Progressive, Voice::Passive) => {
566 format!("will be being {past_participle}")
569 }
570 },
571 }
572}
573
574#[cfg(test)]
575mod tests {
576 use super::PluralCategory;
577 use super::*;
578 use crate::agreement::AgreementFeatures;
579
580 struct MiniLang;
582
583 impl Language for MiniLang {
584 fn pluralize(&self, word: &str, count: usize) -> String {
585 if count == 1 {
586 return word.to_string();
587 }
588 if word.ends_with("ss")
590 || word.ends_with("sh")
591 || word.ends_with("ch")
592 || word.ends_with('x')
593 || word.ends_with('z')
594 {
595 format!("{word}es")
596 } else if word.ends_with('s') {
597 format!("{word}es")
599 } else {
600 format!("{word}s")
601 }
602 }
603 fn singularize(&self, word: &str) -> String {
604 word.strip_suffix('s').unwrap_or(word).to_string()
605 }
606 fn article(&self, _word: &str) -> &str {
607 "a"
608 }
609 fn conjugate(&self, verb: &str, tense: Tense, _person: Person) -> String {
610 match tense {
611 Tense::Past => format!("{verb}ed"),
612 Tense::Present => verb.to_string(),
613 Tense::Future => format!("will {verb}"),
614 }
615 }
616 fn past_participle(&self, verb: &str) -> String {
617 format!("{verb}ed")
618 }
619 fn present_participle(&self, verb: &str) -> String {
620 format!("{verb}ing")
621 }
622 fn join_list(&self, items: &[&str], _conjunction: Conjunction) -> String {
623 items.join(", ")
624 }
625 fn ordinal(&self, n: usize) -> String {
626 format!("{n}th")
627 }
628 fn number_to_words(&self, n: usize) -> String {
629 format!("{n}")
630 }
631 }
632
633 #[test]
634 fn plural_description_default_zero_is_empty() {
635 let l = MiniLang;
636 assert_eq!(
637 l.plural_description("class", 0, &AgreementFeatures::default()),
638 ""
639 );
640 }
641
642 #[test]
643 fn plural_description_default_one_is_the_type() {
644 let l = MiniLang;
645 assert_eq!(
646 l.plural_description("class", 1, &AgreementFeatures::default()),
647 "the class"
648 );
649 }
650
651 #[test]
652 fn plural_description_default_many_uses_pluralize() {
653 let l = MiniLang;
654 assert_eq!(
655 l.plural_description("class", 3, &AgreementFeatures::default()),
656 "the 3 classes"
657 );
658 }
659
660 #[test]
661 fn plural_description_two_items() {
662 let l = MiniLang;
663 assert_eq!(
664 l.plural_description("service", 2, &AgreementFeatures::default()),
665 "the 2 services"
666 );
667 }
668
669 #[test]
670 fn plural_description_ignores_features_in_default_impl() {
671 let l = MiniLang;
674 let with_features = l.plural_description("class", 3, &AgreementFeatures::default());
675 let without = l.plural_description("class", 3, &AgreementFeatures::default());
676 assert_eq!(with_features, without);
677 assert_eq!(with_features, "the 3 classes");
678 }
679
680 #[test]
683 fn default_plural_category_one() {
684 let lang = MiniLang;
685 assert_eq!(lang.plural_category(1), PluralCategory::One);
686 }
687
688 #[test]
689 fn default_plural_category_other_for_zero() {
690 let lang = MiniLang;
691 assert_eq!(lang.plural_category(0), PluralCategory::Other);
692 }
693
694 #[test]
695 fn default_plural_category_other_for_plurals() {
696 let lang = MiniLang;
697 assert_eq!(lang.plural_category(2), PluralCategory::Other);
698 assert_eq!(lang.plural_category(17), PluralCategory::Other);
699 }
700
701 #[test]
702 fn default_plural_category_other_for_negatives() {
703 let lang = MiniLang;
704 assert_eq!(lang.plural_category(-5), PluralCategory::Other);
705 }
706
707 #[test]
708 fn default_pluralize_with_category_one_is_singular() {
709 let lang = MiniLang;
710 assert_eq!(
711 lang.pluralize_with_category("service", PluralCategory::One),
712 "service"
713 );
714 }
715
716 #[test]
717 fn default_pluralize_with_category_other_is_plural() {
718 let lang = MiniLang;
719 assert_eq!(
720 lang.pluralize_with_category("service", PluralCategory::Other),
721 "services"
722 );
723 }
724
725 #[test]
726 fn default_pluralize_with_category_few_falls_to_plural() {
727 let lang = MiniLang;
730 assert_eq!(
731 lang.pluralize_with_category("service", PluralCategory::Few),
732 "services"
733 );
734 }
735
736 #[test]
737 fn default_pluralize_with_category_many_falls_to_plural() {
738 let lang = MiniLang;
739 assert_eq!(
740 lang.pluralize_with_category("service", PluralCategory::Many),
741 "services"
742 );
743 }
744
745 #[test]
746 fn default_pluralize_with_category_zero_falls_to_plural() {
747 let lang = MiniLang;
748 assert_eq!(
749 lang.pluralize_with_category("service", PluralCategory::Zero),
750 "services"
751 );
752 }
753
754 #[test]
755 fn default_pluralize_with_category_two_falls_to_plural() {
756 let lang = MiniLang;
757 assert_eq!(
758 lang.pluralize_with_category("service", PluralCategory::Two),
759 "services"
760 );
761 }
762
763 #[test]
764 fn plural_category_default_variant_is_other() {
765 assert_eq!(PluralCategory::default(), PluralCategory::Other);
766 }
767
768 #[test]
771 fn realize_reference_pronoun_singular() {
772 let lang = MiniLang;
773 let f = AgreementFeatures::default(); assert_eq!(
775 lang.realize_reference(crate::discourse::ReferenceForm::Pronoun, &f),
776 Some("it".to_string())
777 );
778 }
779
780 #[test]
781 fn realize_reference_pronoun_plural() {
782 let lang = MiniLang;
783 let f = AgreementFeatures::default().with_number(crate::agreement::Number::Plural);
784 assert_eq!(
785 lang.realize_reference(crate::discourse::ReferenceForm::Pronoun, &f),
786 Some("they".to_string())
787 );
788 }
789
790 #[test]
791 fn realize_reference_pronoun_dual() {
792 let lang = MiniLang;
793 let f = AgreementFeatures::default().with_number(crate::agreement::Number::Dual);
794 assert_eq!(
795 lang.realize_reference(crate::discourse::ReferenceForm::Pronoun, &f),
796 Some("they".to_string())
797 );
798 }
799
800 #[test]
801 fn realize_reference_possessive_singular() {
802 let lang = MiniLang;
803 let f = AgreementFeatures::default();
804 assert_eq!(
805 lang.realize_reference(crate::discourse::ReferenceForm::Possessive, &f),
806 Some("its".to_string())
807 );
808 }
809
810 #[test]
811 fn realize_reference_possessive_plural() {
812 let lang = MiniLang;
813 let f = AgreementFeatures::default().with_number(crate::agreement::Number::Plural);
814 assert_eq!(
815 lang.realize_reference(crate::discourse::ReferenceForm::Possessive, &f),
816 Some("their".to_string())
817 );
818 }
819
820 #[test]
821 fn realize_reference_demonstrative() {
822 let lang = MiniLang;
823 let f = AgreementFeatures::default();
824 assert_eq!(
825 lang.realize_reference(crate::discourse::ReferenceForm::Demonstrative, &f),
826 Some("this".to_string())
827 );
828 }
829
830 #[test]
831 fn realize_reference_zero_is_none() {
832 let lang = MiniLang;
833 let f = AgreementFeatures::default();
834 assert_eq!(
835 lang.realize_reference(crate::discourse::ReferenceForm::Zero, &f),
836 None
837 );
838 }
839
840 #[test]
841 fn realize_reference_full_is_none() {
842 let lang = MiniLang;
844 let f = AgreementFeatures::default();
845 assert_eq!(
846 lang.realize_reference(crate::discourse::ReferenceForm::Full, &f),
847 None
848 );
849 }
850
851 #[test]
852 fn realize_reference_short_name_is_none() {
853 let lang = MiniLang;
855 let f = AgreementFeatures::default();
856 assert_eq!(
857 lang.realize_reference(crate::discourse::ReferenceForm::ShortName, &f),
858 None
859 );
860 }
861
862 #[test]
863 fn possessive_name_adds_english_suffix() {
864 let lang = MiniLang;
865 assert_eq!(lang.possessive_name("UserService"), "UserService's");
866 assert_eq!(lang.possessive_name("CoreServices"), "CoreServices'");
867 }
868
869 #[test]
872 fn proportion_phrase_default_delegates_to_english_both() {
873 let lang = MiniLang;
874 let f = AgreementFeatures::default();
875 assert_eq!(
876 lang.proportion_phrase(2, 2, Some("modified file"), &f),
877 "both modified files"
878 );
879 }
880
881 #[test]
882 fn proportion_phrase_default_delegates_to_english_all_n() {
883 let lang = MiniLang;
884 let f = AgreementFeatures::default();
885 assert_eq!(
886 lang.proportion_phrase(13, 13, Some("modified file"), &f),
887 "all 13 modified files"
888 );
889 }
890
891 #[test]
892 fn proportion_phrase_default_partial_without_noun() {
893 let lang = MiniLang;
894 let f = AgreementFeatures::default();
895 assert_eq!(lang.proportion_phrase(3, 13, None, &f), "3 of 13");
896 }
897
898 #[test]
899 fn proportion_phrase_default_zero_zero_with_noun() {
900 let lang = MiniLang;
901 let f = AgreementFeatures::default();
902 assert_eq!(lang.proportion_phrase(0, 0, Some("file"), &f), "no files");
903 }
904
905 #[test]
906 fn discourse_marker_english_defaults() {
907 let lang = MiniLang;
908 use crate::rst::RstRelation::*;
909 assert_eq!(lang.discourse_marker(Elaboration), Some("Furthermore, "));
910 assert_eq!(lang.discourse_marker(Contrast), Some("However, "));
911 assert_eq!(lang.discourse_marker(Result), Some("As a result, "));
912 }
913
914 #[cfg(feature = "time")]
917 #[test]
918 fn since_last_marker_default_the_next_day() {
919 let lang = MiniLang;
920 assert_eq!(lang.since_last_marker(86_400 + 1), "the next day");
921 }
922
923 #[cfg(feature = "time")]
924 #[test]
925 fn since_last_marker_default_moments_later() {
926 let lang = MiniLang;
927 assert_eq!(lang.since_last_marker(30), "moments later");
928 }
929
930 #[cfg(feature = "time")]
931 #[test]
932 fn since_last_marker_default_zero_is_at_the_same_time() {
933 let lang = MiniLang;
934 assert_eq!(lang.since_last_marker(0), "at the same time");
935 }
936
937 #[cfg(feature = "time")]
938 #[test]
939 fn since_last_marker_default_years() {
940 let lang = MiniLang;
941 assert_eq!(lang.since_last_marker(3 * 365 * 86_400), "3 years later");
942 }
943}