001package squidpony;
002
003import regexodus.*;
004import java.util.HashMap;
005
006/**
007 * Helps handle formation of messages from a template, using correct pronouns and helping handle various idiosyncrasies
008 * in English-language text. You call the static method
009 * {@link #transform(CharSequence, String, NounTrait, String, NounTrait)} (or one of its overloads) with a template that
010 * has specific placeholder glyphs, along with a user name, optional target name, user NounTrait (an enum in this class)
011 * to specify how the user should be addressed, including their gender, optional target NounTrait, and possibly extra
012 * terms that should be inserted. The placeholder glyphs are usually followed by a specific word that is conjugated in
013 * the template for the first-person case, and will be changed to fit the NounTrait for the user or target. For example,
014 * you could use "@Name hit$ ^ for ~ damage!" as a message. You could transform it with user "Heero Supra", userTrait
015 * NounTrait.SECOND_PERSON_SINGULAR, target "the beast", targetTrait NounTrait.UNSPECIFIED_GENDER, and extra "10" to get
016 * the message "You hit the beast for 10 damage!". You could swap the user and target (along with their traits) to get
017 * the message "The beast hits you for 10 damage!" You can handle more complex verbs in some cases, such as "@I hurr$$$
018 * to catch up!" can be transformed to "You hurry to catch up!" or "He hurries to catch up!". The rules are fairly
019 * simple; @word conjugates a specific word from a list to the correct kind for the user, while ^word does a similar
020 * thing but conjugates for the target. Between 1 and 3 $ chars can be used at the end of verbs to conjugate them
021 * appropriately for the present tense when the verb is performed by the user (with just $) or alternately the target
022 * (if the $ chars are preceded by a ^), while @s, @ss, @sss, ^s, ^ss, or ^sss can be added at the end of nouns to
023 * pluralize them if appropriate. Using one $ or s will add s or nothing, as in the case of hit becoming hits, using two
024 * $ or s chars will add es or nothing, as in the case of scratch becoming scratches, and using three will add ies or y,
025 * as in the case of carry becoming carries. Some unusual pluralization forms are handled; @usi will turn octop@usi into
026 * octopus or octopi, and radi@usi into radius or radii, while @fves will turn el@fves into elf or elves, or dwar@fves
027 * into dwarf or dwarves.
028 * <br>
029 * The words you can put after a @ or ^ start with a small list and can be added to with
030 * {@link #learnIrregularWord(String, String, String, String, String, String, String)}. The initial list is: name,
031 * name_s, i, me, my, mine, myself, am, have, do, haven_t, don_t, or any of these with the first char capitalized (meant
032 * for words at the start of sentences). The non-word shortened terms "m" and "ve" can be used for "I'm" and "I've",
033 * respectively, as well as "you're" and "you've", plus "he's" for "he is" and "he's" for "he has". Most of the rest
034 * conjugate as you would expect; @me will become him, her, it, them, you, or still more forms depending on userTrait.
035 * You can also use @ or ^ on its own as an equivalent to @name or ^name.
036 * <br>
037 * Examples:
038 * <br>
039 * {@code Messaging.transform("@I @am @my own boss@ss.", "unused", changingTrait)}
040 * <ul>
041 *     <li>When changingTrait is {@code NounTrait.FIRST_PERSON_SINGULAR}, this returns "I am my own boss."</li>
042 *     <li>When changingTrait is {@code NounTrait.FIRST_PERSON_PLURAL}, this returns "We are our own bosses."</li>
043 *     <li>When changingTrait is {@code NounTrait.SECOND_PERSON_SINGULAR}, this returns "You are your own boss."</li>
044 *     <li>When changingTrait is {@code NounTrait.SECOND_PERSON_PLURAL}, this returns "You are your own bosses."</li>
045 *     <li>When changingTrait is {@code NounTrait.NO_GENDER}, this returns "It is its own boss."</li>
046 *     <li>When changingTrait is {@code NounTrait.MALE_GENDER}, this returns "He is his own boss."</li>
047 *     <li>When changingTrait is {@code NounTrait.FEMALE_GENDER}, this returns "She is her own boss."</li>
048 *     <li>When changingTrait is {@code NounTrait.UNSPECIFIED_GENDER}, this returns "They are their own boss."</li>
049 *     <li>When changingTrait is {@code NounTrait.ADDITIONAL_GENDER}, this returns "Xe is xis own boss."</li>
050 *     <li>When changingTrait is {@code NounTrait.SPECIAL_CASE_GENDER}, this returns "Qvqe is qvqis own boss."</li>
051 *     <li>When changingTrait is {@code NounTrait.GROUP}, this returns "They are their own bosses."</li>
052 * </ul>
053 * {@code Messaging.transform("@Name spit$ in ^name_s face^s!", userName, userTrait, targetName, targetTrait)}
054 * <ul>
055 *     <li>When userTrait is {@code NounTrait.SECOND_PERSON_SINGULAR}, targetName is {@code "the goblin"}, and
056 *     targetTrait is {@code NounTrait.MALE_GENDER}, this returns "You spit in the goblin's face!"</li>
057 *     <li>When userName is {@code "the goblin"}, userTrait is {@code NounTrait.MALE_GENDER}, and targetTrait is
058 *     {@code NounTrait.SECOND_PERSON_SINGULAR}, this returns "The goblin spits in your face!"</li>
059 *     <li>When userTrait is {@code NounTrait.SECOND_PERSON_SINGULAR}, targetName is {@code "the goblins"}, and
060 *     targetTrait is {@code NounTrait.GROUP}, this returns "You spit in the goblins' faces!"</li>
061 *     <li>When userName is {@code "the goblins"}, userTrait is {@code NounTrait.GROUP}, and targetTrait is
062 *     {@code NounTrait.SECOND_PERSON_SINGULAR}, this returns "The goblins spit in your face!"</li>
063 * </ul>
064 * Created by Tommy Ettinger on 10/31/2016.
065 */
066public class Messaging {
067
068    /**
069     * Properties of nouns needed to correctly conjugate those nouns and refer to them with pronouns, such as genders.
070     * Includes parts of speech, which only are concerned with whether they refer to a singular noun or a plural noun,
071     * and genders for when a gendered pronoun is needed. This provides substantial support for uncommon cases regarding
072     * gender and pronoun preferences. That said, gender and pronoun preference can be incredibly hard to handle.
073     * The simplest cases are for first- and second-person pronouns; here we have "I/me/my/myself" for
074     * {@link #FIRST_PERSON_SINGULAR}, "you/you/your/yourself" for {@link #SECOND_PERSON_SINGULAR},
075     * "we/us/our/ourselves" for {@link #FIRST_PERSON_PLURAL}, and "you/you/your/yourselves" for
076     * {@link #SECOND_PERSON_PLURAL}; there are more pronouns this can produce, but they aren't listed here.
077     * Third-person pronouns are considerably more challenging because English sporadically considers gender as part of
078     * conjugation, but doesn't provide a universally-acceptable set of gendered pronouns.
079     * <br>
080     * This at least tries to provide pronoun handling for the common cases, such as "you" not needing a gendered
081     * pronoun at all (it uses {@link #SECOND_PERSON_SINGULAR}), and supports {@link #MALE_GENDER male},
082     * {@link #FEMALE_GENDER female}, {@link #NO_GENDER genderless} (using "it" and related forms; preferred especially
083     * for things that aren't alive, and in most cases not recommended for people),
084     * {@link #UNSPECIFIED_GENDER "unspecified"} (using "they" in place of "he" or "she"; preferred in some cases when
085     * describing someone with a non-specific gender or an unknown gender) pronouns, and {@link #GROUP group} for when a
086     * group of individuals, regardless of gender or genders, is referred to with a single pronoun. As mentioned, this
087     * has support for some uncommon situations, like {@link #ADDITIONAL_GENDER additional gender} (as in, a gender that
088     * is in addition to male and female but that is not genderless, which has a clear use case when describing
089     * non-human species, and a more delicate use for humans who use non-binary gender pronouns; hopefully "xe" will be
090     * acceptable), and finally a {@link #SPECIAL_CASE_GENDER "special case"} pronoun that is unpronounceable and, if
091     * given special processing, can be used as a replacement target for customized pronouns. For the additional gender,
092     * the non-binary gendered pronouns are modified from the male pronouns by replacing 'h' with 'x' (he becomes xe,
093     * his becomes xis). The "special case" pronouns replace the 'h' in the male pronouns with 'qvq', except for in one
094     * case. Where, if the female pronoun were used, it would be "hers", but the male pronoun in that case would be "his",
095     * changing the male pronoun would lead to a difficult-to-replace case because "his" is also used in the case where
096     * the female pronoun is the usefully distinct "her". Here, the "special case" gender diverges from what it usually
097     * does, and uses "qvqims" in place of "his" or "hers". The "special case" pronouns should be replaced before being
098     * displayed, since they look like gibberish or a glitch and so are probably confusing out of context.
099     */
100    public enum NounTrait {
101        /**
102         * As in, "I am my own boss." Doesn't reference gender.
103         */
104        FIRST_PERSON_SINGULAR,
105        /**
106         * As in, "You are your own boss." Doesn't reference gender.
107         */
108        SECOND_PERSON_SINGULAR,
109        /**
110         * As in, "We are our own bosses." Doesn't reference gender, and applies to groups.
111         */
112        FIRST_PERSON_PLURAL,
113        /**
114         * As in, "You are your own bosses." Doesn't reference gender, and applies to groups.
115         */
116        SECOND_PERSON_PLURAL,
117        /**
118         * Inanimate objects or beings without gender, as in "It is its own boss."
119         */
120        NO_GENDER,
121        /**
122         * Male pronoun preference, as in "He is his own boss."
123         */
124        MALE_GENDER,
125        /**
126         * Female pronoun preference, as in "She is her own boss."
127         */
128        FEMALE_GENDER,
129        /**
130         * "Singular they" pronoun preference or to be used when preference is unknown, as in "They are their own boss."
131         */
132        UNSPECIFIED_GENDER,
133        /**
134         * Third-gender pronoun preference, potentially relevant for cultures with non-binary gender terms. As in, "Xe
135         * is xis own boss."
136         */
137        ADDITIONAL_GENDER,
138        /**
139         * Unpronounceable words that can be processed specially for more complex cases of pronoun preference. As in,
140         * "Qvqe is qvqis own boss."
141         */
142        SPECIAL_CASE_GENDER,
143        /**
144         * Any third-person plural, as in "They are their own bosses." Not to be confused with UNSPECIFIED_GENDER, which
145         * is for singular beings, but usually uses "they" in the same way (not always).
146         */
147        GROUP;
148
149        public String nameText(String term) {
150            switch (this) {
151                case FIRST_PERSON_SINGULAR:
152                    return "I";
153                case FIRST_PERSON_PLURAL:
154                    return "we";
155                case SECOND_PERSON_SINGULAR:
156                case SECOND_PERSON_PLURAL:
157                    return "you";
158                default:
159                    return term;
160            }
161        }
162        public String name_sText(String term) {
163            switch (this) {
164                case FIRST_PERSON_SINGULAR:
165                    return "my";
166                case FIRST_PERSON_PLURAL:
167                    return "our";
168                case SECOND_PERSON_SINGULAR:
169                case SECOND_PERSON_PLURAL:
170                    return "your";
171                default:
172                    if(term.isEmpty()) return "";
173                    else if(term.endsWith("s")) return term + '\'';
174                    else return term + "'s";
175            }
176        }
177
178        public String iText() {
179            switch (this) {
180                case FIRST_PERSON_SINGULAR:
181                    return "I";
182                case FIRST_PERSON_PLURAL:
183                    return "we";
184                case SECOND_PERSON_SINGULAR:
185                case SECOND_PERSON_PLURAL:
186                    return "you";
187                case NO_GENDER:
188                    return "it";
189                case MALE_GENDER:
190                    return "he";
191                case FEMALE_GENDER:
192                    return "she";
193                //case UNSPECIFIED_GENDER: return "they";
194                case ADDITIONAL_GENDER:
195                    return "xe";
196                case SPECIAL_CASE_GENDER:
197                    return "qvqe";
198                default:
199                    return "they";
200            }
201        }
202        public String meText() {
203            switch (this) {
204                case FIRST_PERSON_SINGULAR:
205                    return "me";
206                case FIRST_PERSON_PLURAL:
207                    return "us";
208                case SECOND_PERSON_SINGULAR:
209                case SECOND_PERSON_PLURAL:
210                    return "you";
211                case NO_GENDER:
212                    return "it";
213                case MALE_GENDER:
214                    return "him";
215                case FEMALE_GENDER:
216                    return "her";
217                //case UNSPECIFIED_GENDER: return "them";
218                case ADDITIONAL_GENDER:
219                    return "xim";
220                case SPECIAL_CASE_GENDER:
221                    return "qvqim";
222                default:
223                    return "them";
224            }
225        }
226
227        public String myText() {
228            switch (this) {
229                case FIRST_PERSON_SINGULAR:
230                    return "my";
231                case FIRST_PERSON_PLURAL:
232                    return "our";
233                case SECOND_PERSON_SINGULAR:
234                case SECOND_PERSON_PLURAL:
235                    return "your";
236                case NO_GENDER:
237                    return "its";
238                case MALE_GENDER:
239                    return "his";
240                case FEMALE_GENDER:
241                    return "her";
242                //case UNSPECIFIED_GENDER: return "their";
243                case ADDITIONAL_GENDER:
244                    return "xis";
245                case SPECIAL_CASE_GENDER:
246                    return "qvqis";
247                default:
248                    return "their";
249            }
250        }
251        public String mineText() {
252            switch (this) {
253                case FIRST_PERSON_SINGULAR:
254                    return "mine";
255                case FIRST_PERSON_PLURAL:
256                    return "ours";
257                case SECOND_PERSON_SINGULAR:
258                case SECOND_PERSON_PLURAL:
259                    return "yours";
260                case NO_GENDER:
261                    return "its";
262                case MALE_GENDER:
263                    return "his";
264                case FEMALE_GENDER:
265                    return "hers";
266                //case UNSPECIFIED_GENDER: return "theirs";
267                case ADDITIONAL_GENDER:
268                    return "xis";
269                case SPECIAL_CASE_GENDER:
270                    return "qvqims";
271                default:
272                    return "theirs";
273            }
274        }
275
276        public String myselfText() {
277            switch (this) {
278                case FIRST_PERSON_SINGULAR:
279                    return "myself";
280                case FIRST_PERSON_PLURAL:
281                    return "ourselves";
282                case SECOND_PERSON_SINGULAR:
283                    return "yourself";
284                case SECOND_PERSON_PLURAL:
285                    return "yourselves";
286                case NO_GENDER:
287                    return "itself";
288                case MALE_GENDER:
289                    return "himself";
290                case FEMALE_GENDER:
291                    return "herself";
292                case UNSPECIFIED_GENDER:
293                    return "themself";
294                case ADDITIONAL_GENDER:
295                    return "ximself";
296                case SPECIAL_CASE_GENDER:
297                    return "qvqimself";
298                default:
299                    return "themselves";
300            }
301        }
302
303        public String sText() {
304            switch (this) {
305                case FIRST_PERSON_PLURAL:
306                case SECOND_PERSON_PLURAL:
307                case GROUP:
308                    return "s";
309                default:
310                    return "";
311            }
312        }
313        public String ssText() {
314            switch (this) {
315                case FIRST_PERSON_PLURAL:
316                case SECOND_PERSON_PLURAL:
317                case GROUP:
318                    return "es";
319                default:
320                    return "";
321            }
322        }
323        public String sssText() {
324            switch (this) {
325                case FIRST_PERSON_PLURAL:
326                case SECOND_PERSON_PLURAL:
327                case GROUP:
328                    return "ies";
329                default:
330                    return "y";
331            }
332        }
333        public String usiText() {
334            switch (this) {
335                case FIRST_PERSON_PLURAL:
336                case SECOND_PERSON_PLURAL:
337                case GROUP:
338                    return "i";
339                default:
340                    return "us";
341            }
342        }
343        public String fvesText() {
344            switch (this) {
345                case FIRST_PERSON_PLURAL:
346                case SECOND_PERSON_PLURAL:
347                case GROUP:
348                    return "ves";
349                default:
350                    return "f";
351            }
352        }
353
354        public String $Text() {
355            switch (this) {
356                case FIRST_PERSON_SINGULAR:
357                case FIRST_PERSON_PLURAL:
358                case SECOND_PERSON_SINGULAR:
359                case SECOND_PERSON_PLURAL:
360                case UNSPECIFIED_GENDER:
361                case GROUP:
362                    return "";
363                default:
364                    return "s";
365            }
366        }
367        public String $$Text() {
368            switch (this) {
369                case FIRST_PERSON_SINGULAR:
370                case FIRST_PERSON_PLURAL:
371                case SECOND_PERSON_SINGULAR:
372                case SECOND_PERSON_PLURAL:
373                case UNSPECIFIED_GENDER:
374                case GROUP:
375                    return "";
376                default:
377                    return "es";
378            }
379        }
380        public String $$$Text() {
381            switch (this) {
382                case FIRST_PERSON_SINGULAR:
383                case FIRST_PERSON_PLURAL:
384                case SECOND_PERSON_SINGULAR:
385                case SECOND_PERSON_PLURAL:
386                case UNSPECIFIED_GENDER:
387                case GROUP:
388                    return "y";
389                default:
390                    return "ies";
391            }
392        }
393
394        public String directText(String term) {
395            return term;
396        }
397
398    }
399
400    public static class Group
401    {
402        public String[] members;
403
404        public Group()
405        {
406            members = new String[]{"the goblin", "the dragon", "the warlock"};
407        }
408        public Group(String... members)
409        {
410            this.members = members;
411        }
412    }
413
414
415    /**
416     * Takes message and replaces any of the special terms this recognizes, like @, ^, and $, with the appropriately-
417     * conjugated terms for the given user and their associated NounTrait.
418     * @param message the message to transform; should contain "@" or "$" in it, at least, to be replaced
419     * @param user the name of the user for cases where it can replace text like "@" or "@Name"
420     * @param userTrait the {@link NounTrait} enum that determines how user should be referred to
421     * @return a String resulting from the processing of message
422     */
423    public static String transform(CharSequence message, String user, NounTrait userTrait)
424    {
425        Replacer ur = new Replacer(userPattern, new BeingSubstitution(user, userTrait, true));
426        return ur.replace(message);
427    }
428
429    /**
430     * Takes message and replaces any of the special terms this recognizes, like @, ^, and $, with the appropriately-
431     * conjugated terms for the given group of users and that group's associated NounTrait. The NounTrait only matters
432     * if it is first-person or second-person (in which case this uses the plural form) or if the Group contains one
433     * member (in which case it uses any gendered pronouns specified by userTrait); it uses {@link NounTrait#GROUP} in
434     * any other case.
435     * @param message the message to transform; should contain "@" or "$" in it, at least, to be replaced
436     * @param user a {@link Group} of users (a String array) for cases where it can replace text, like "@" or "@Name"
437     * @param userTrait the {@link NounTrait} enum that determines how user should be referred to
438     * @return a String resulting from the processing of message
439     */
440    public static String transform(CharSequence message, Group user, NounTrait userTrait)
441    {
442        Replacer ur = new Replacer(userPattern, new BeingSubstitution(userTrait, true, user.members));
443        return ur.replace(message);
444    }
445
446    /**
447     * Takes message and replaces any of the special terms this recognizes, like @, ^, and $, with the appropriately-
448     * conjugated terms for the given user, their associated NounTrait, the given target, and their NounTrait.
449     * @param message the message to transform; should contain "@", "^", or "$" in it, at least, to be replaced
450     * @param user the name of the user for cases where it can replace text like "@" or "@Name"
451     * @param userTrait the {@link NounTrait} enum that determines how user should be referred to
452     * @param target the name of the target for cases where it can replace text like "^" or "^Name"
453     * @param targetTrait the {@link NounTrait} enum that determines how the target should be referred to
454     * @return a String resulting from the processing of message
455     */
456    public static String transform(CharSequence message, String user, NounTrait userTrait, String target, NounTrait targetTrait)
457    {
458        Replacer tr = new Replacer(targetPattern, new BeingSubstitution(target, targetTrait, false)),
459                ur = new Replacer(userPattern, new BeingSubstitution(user, userTrait, true));
460        return ur.replace(tr.replace(message));
461    }
462
463    /**
464     * Takes message and replaces any of the special terms this recognizes, like @, ^, and $, with the appropriately-
465     * conjugated terms for the given Group of users, that group's associated NounTrait, the given target, and their
466     * NounTrait. The NounTrait for user only matters if it is first-person or second-person (in which case this uses
467     * the plural form) or if the Group contains one member (in which case it uses any gendered pronouns specified by
468     * userTrait); it uses {@link NounTrait#GROUP} in any other case.
469     * @param message the message to transform; should contain "@", "^", or "$" in it, at least, to be replaced
470     * @param user the {@link Group} of users for cases where they can replace text like "@" or "@Name"
471     * @param userTrait the {@link NounTrait} enum that determines how user should be referred to
472     * @param target the name of the target for cases where it can replace text like "^" or "^Name"
473     * @param targetTrait the {@link NounTrait} enum that determines how the target should be referred to
474     * @return a String resulting from the processing of message
475     */
476    public static String transform(CharSequence message, Group user, NounTrait userTrait, String target, NounTrait targetTrait)
477    {
478        Replacer tr = new Replacer(targetPattern, new BeingSubstitution(target, targetTrait, false)),
479                ur = new Replacer(userPattern, new BeingSubstitution(userTrait, true, user.members));
480        return ur.replace(tr.replace(message));
481    }
482
483    /**
484     * Takes message and replaces any of the special terms this recognizes, like @, ^, and $, with the appropriately-
485     * conjugated terms for the given Group of users, that group's associated NounTrait, the given group of targets, and
486     * that group's NounTrait. The NounTraits only matter if they are is first-person or second-person (in which case
487     * this uses the plural form) or if a Group contains one member (in which case it uses any gendered pronouns
488     * specified by userTrait or targetTrait); it uses {@link NounTrait#GROUP} in any other case.
489     * @param message the message to transform; should contain "@", "^", or "$" in it, at least, to be replaced
490     * @param user the {@link Group} of users for cases where they can replace text like "@" or "@Name"
491     * @param userTrait the {@link NounTrait} enum that determines how user should be referred to
492     * @param target the {@link Group} of targets for cases where they can replace text like "@" or "@Name"
493     * @param targetTrait the {@link NounTrait} enum that determines how the target should be referred to
494     * @return a String resulting from the processing of message
495     */
496    public static String transform(CharSequence message, Group user, NounTrait userTrait, Group target, NounTrait targetTrait)
497    {
498        Replacer tr = new Replacer(targetPattern, new BeingSubstitution(targetTrait, false, target.members)),
499                ur = new Replacer(userPattern, new BeingSubstitution(userTrait, true, user.members));
500        return ur.replace(tr.replace(message));
501    }
502
503
504    /**
505     * Takes message and replaces any of the special terms this recognizes, like @, ^, and $, with the appropriately-
506     * conjugated terms for the given user, that user's associated NounTrait, the given Group of targets, and that
507     * group's NounTrait. The NounTrait for target only matters if it is first-person or second-person (in which case
508     * this uses the plural form) or if the Group contains one member (in which case it uses any gendered pronouns
509     * specified by targetTrait); it uses {@link NounTrait#GROUP} in any other case.
510     * @param message the message to transform; should contain "@", "^", or "$" in it, at least, to be replaced
511     * @param user the name of the user for cases where it can replace text like "@" or "@Name"
512     * @param userTrait the {@link NounTrait} enum that determines how user should be referred to
513     * @param target the {@link Group} of targets for cases where they can replace text like "@" or "@Name"
514     * @param targetTrait the {@link NounTrait} enum that determines how the target should be referred to
515     * @return a String resulting from the processing of message
516     */
517    public static String transform(CharSequence message, String user, NounTrait userTrait, Group target, NounTrait targetTrait)
518    {
519        Replacer tr = new Replacer(targetPattern, new BeingSubstitution(targetTrait, false, target.members)),
520                ur = new Replacer(userPattern, new BeingSubstitution(user, userTrait, true));
521        return ur.replace(tr.replace(message));
522    }
523
524    /**
525     * Takes message and replaces any of the special terms this recognizes, like @, ^, and $, with the appropriately-
526     * conjugated terms for the given user, their associated NounTrait, the given target, and their NounTrait. Also
527     * replaces the nth occurrence of "~" with the matching nth item in extra, so the first "~" is replaced with the
528     * first item in extra, the second "~" with the second item, and so on until one is exhausted.
529     * @param message the message to transform; should contain "@", "^", "$", or "~" in it, at least, to be replaced
530     * @param user the name of the user for cases where it can replace text like "@" or "@Name"
531     * @param userTrait the {@link NounTrait} enum that determines how user should be referred to
532     * @param target the name of the target for cases where it can replace text like "^" or "^Name"
533     * @param targetTrait the {@link NounTrait} enum that determines how the target should be referred to
534     * @param extra an array or vararg of String where the nth item in extra will replace the nth occurrence of "~"
535     * @return a String resulting from the processing of message
536     */
537    public static String transform(CharSequence message, String user, NounTrait userTrait, String target, NounTrait targetTrait, String... extra)
538    {
539        Replacer tr = new Replacer(targetPattern, new BeingSubstitution(target, targetTrait, false)),
540                ur = new Replacer(userPattern, new BeingSubstitution(user, userTrait, true));
541        String text = ur.replace(tr.replace(message));
542        if(extra != null && extra.length > 0)
543        {
544            for (int i = 0; i < extra.length; i++) {
545                text = text.replaceFirst("~", extra[i]);
546            }
547        }
548        return text;
549    }
550
551    /**
552     * Takes message and replaces any of the special terms this recognizes, like @, ^, and $, with the appropriately-
553     * conjugated terms for the given Group of users, that group's associated NounTrait, the given target, and their
554     * NounTrait. Also replaces the nth occurrence of "~" with the matching nth item in extra, so the first "~" is
555     * replaced with the first item in extra, the second "~" with the second item, and so on until one is exhausted. The
556     * NounTrait for user only matters if it is first-person or second-person (in which case this uses the plural form)
557     * or if the Group contains one member (in which case it uses any gendered pronouns specified by userTrait); it uses
558     * {@link NounTrait#GROUP} in any other case.
559     * @param message the message to transform; should contain "@", "^", or "$" in it, at least, to be replaced
560     * @param user the {@link Group} of users for cases where they can replace text like "@" or "@Name"
561     * @param userTrait the {@link NounTrait} enum that determines how user should be referred to
562     * @param target the name of the target for cases where it can replace text like "^" or "^Name"
563     * @param targetTrait the {@link NounTrait} enum that determines how the target should be referred to
564     * @param extra an array or vararg of String where the nth item in extra will replace the nth occurrence of "~"
565     * @return a String resulting from the processing of message
566     */
567    public static String transform(CharSequence message, Group user, NounTrait userTrait, String target, NounTrait targetTrait, String... extra)
568    {
569        Replacer tr = new Replacer(targetPattern, new BeingSubstitution(target, targetTrait, false)),
570                ur = new Replacer(userPattern, new BeingSubstitution(userTrait, true, user.members));
571        String text = ur.replace(tr.replace(message));
572        if(extra != null && extra.length > 0)
573        {
574            for (int i = 0; i < extra.length; i++) {
575                text = text.replaceFirst("~", extra[i]);
576            }
577        }
578        return text;
579    }
580
581    /**
582     * Takes message and replaces any of the special terms this recognizes, like @, ^, and $, with the appropriately-
583     * conjugated terms for the given Group of users, that group's associated NounTrait, the given group of targets, and
584     * that group's NounTrait. Also replaces the nth occurrence of "~" with the matching nth item in extra, so the first
585     * "~" is replaced with the first item in extra, the second "~" with the second item, and so on until one is
586     * exhausted. The NounTraits only matter if they are is first-person or second-person (in which case
587     * this uses the plural form) or if a Group contains one member (in which case it uses any gendered pronouns
588     * specified by userTrait or targetTrait); it uses {@link NounTrait#GROUP} in any other case.
589     * @param message the message to transform; should contain "@", "^", or "$" in it, at least, to be replaced
590     * @param user the {@link Group} of users for cases where they can replace text like "@" or "@Name"
591     * @param userTrait the {@link NounTrait} enum that determines how user should be referred to
592     * @param target the {@link Group} of targets for cases where they can replace text like "@" or "@Name"
593     * @param targetTrait the {@link NounTrait} enum that determines how the target should be referred to
594     * @param extra an array or vararg of String where the nth item in extra will replace the nth occurrence of "~"
595     * @return a String resulting from the processing of message
596     */
597    public static String transform(CharSequence message, Group user, NounTrait userTrait, Group target, NounTrait targetTrait, String... extra)
598    {
599        Replacer tr = new Replacer(targetPattern, new BeingSubstitution(targetTrait, false, target.members)),
600                ur = new Replacer(userPattern, new BeingSubstitution(userTrait, true, user.members));
601        String text = ur.replace(tr.replace(message));
602        if(extra != null && extra.length > 0)
603        {
604            for (int i = 0; i < extra.length; i++) {
605                text = text.replaceFirst("~", extra[i]);
606            }
607        }
608        return text;
609    }
610
611
612    /**
613     * Takes message and replaces any of the special terms this recognizes, like @, ^, and $, with the appropriately-
614     * conjugated terms for the given user, that user's associated NounTrait, the given Group of targets, and that
615     * group's NounTrait. Also replaces the nth occurrence of "~" with the matching nth item in extra, so the first "~"
616     * is replaced with the first item in extra, the second "~" with the second item, and so on until one is exhausted.
617     * The NounTrait for target only matters if it is first-person or second-person (in which case this uses the plural
618     * form) or if the Group contains one member (in which case it uses any gendered pronouns specified by targetTrait);
619     * it uses {@link NounTrait#GROUP} in any other case.
620     * @param message the message to transform; should contain "@", "^", or "$" in it, at least, to be replaced
621     * @param user the name of the user for cases where it can replace text like "@" or "@Name"
622     * @param userTrait the {@link NounTrait} enum that determines how user should be referred to
623     * @param target the {@link Group} of targets for cases where they can replace text like "@" or "@Name"
624     * @param targetTrait the {@link NounTrait} enum that determines how the target should be referred to
625     * @param extra an array or vararg of String where the nth item in extra will replace the nth occurrence of "~"
626     * @return a String resulting from the processing of message
627     */
628    public static String transform(CharSequence message, String user, NounTrait userTrait, Group target, NounTrait targetTrait, String... extra)
629    {
630        Replacer tr = new Replacer(targetPattern, new BeingSubstitution(targetTrait, false, target.members)),
631                ur = new Replacer(userPattern, new BeingSubstitution(user, userTrait, true));
632        String text = ur.replace(tr.replace(message));
633        if(extra != null && extra.length > 0)
634        {
635            for (int i = 0; i < extra.length; i++) {
636                text = text.replaceFirst("~", extra[i]);
637            }
638        }
639        return text;
640    }
641
642
643    protected static final Pattern
644            userPattern = Pattern.compile("({at_sign}\\\\@)|({caret_sign}\\\\\\^)|({dollar_sign}\\\\\\$)|({tilde_sign}\\\\~)|" +
645            "({$$$}\\$\\$\\$)|({$$}\\$\\$)|({$}\\$)|({sss}@sss\\b)|({ss}@ss\\b)|({s}@s\\b)|({usi}@usi\\b)|({fves}@fves\\b)|" +
646            "({name_s}@name_s\\b)|({Name_s}@Name_s\\b)|({name}@name\\b)|({Name}@Name\\b)|({i}@i\\b)|({I}@I\\b)|({me}@me\\b)|({Me}@Me\\b)|" +
647            "({myself}@myself\\b)|({Myself}@Myself\\b)|({my}@my\\b)|({My}@My\\b)|({mine}@mine\\b)|({Mine}@Mine\\b)|({direct}@direct\\b)|" +
648            "({Direct}@Direct\\b)|(?:@({Other}\\p{Lu}\\w*)\\b)|(?:@({other}\\p{Ll}\\w*)\\b)|({=name}@)"),
649            targetPattern = Pattern.compile("({at_sign}\\\\@)|({caret_sign}\\\\\\^)|({dollar_sign}\\\\\\$)|({tilde_sign}\\\\~)|" +
650                    "({$$$}\\^\\$\\$\\$)|({$$}\\^\\$\\$)|({$}\\^\\$)|({sss}\\^sss\\b)|({ss}\\^ss\\b)|({s}\\^s\\b)|({usi}\\^usi\\b)|({fves}\\^fves\\b)|" +
651                    "({name_s}\\^name_s\\b)|({Name_s}\\^Name_s\\b)|({name}\\^name\\b)|({Name}\\^Name\\b)|({i}\\^i\\b)|({I}\\^I\\b)|({me}\\^me\\b)|({Me}\\^Me\\b)|" +
652                    "({myself}\\^myself\\b)|({Myself}\\^Myself\\b)|({my}\\^my\\b)|({My}\\^My\\b)|({mine}\\^mine\\b)|({Mine}\\^Mine\\b)|({direct}^direct\\b)|" +
653                    "({Direct}^Direct\\b)|(?:\\^({Other}\\p{Lu}\\w*)\\b)|(?:\\^({other}\\p{Ll}\\w*)\\b)|({=name}\\^)");
654
655    private static final HashMap<String, String[]> irregular = new HashMap<>(64);
656
657    /**
658     * Adds a given {@code word}, which should start with a lower-case letter and use lower-case letters and underscores
659     * only, to the dictionary this stores. The 6 additional arguments are used for first person singular ("I am"),
660     * first person plural ("we are"), second person singular ("you are"), second person plural ("you are", the same
661     * as the last one usually, but not always), third person singular ("he is"), third person plural ("they are").
662     * @param word the word to learn; must start with a letter and use only lower-case letters and underscores
663     * @param firstPersonSingular the conjugated form of the word for first-person singular ("I do", "I am")
664     * @param firstPersonPlural the conjugated form of the word for first-person plural ("we do", "we are")
665     * @param secondPersonSingular the conjugated form of the word for second-person singular ("you do", "you are")
666     * @param secondPersonPlural the conjugated form of the word for second-person plural ("you do", "you are")
667     * @param thirdPersonSingular the conjugated form of the word for third-person singular ("he does", "he is")
668     * @param thirdPersonPlural the conjugated form of the word for third-person plural and unspecified-gender singular ("they do", "they are")
669     */
670    public static void learnIrregularWord(String word, String firstPersonSingular, String firstPersonPlural,
671                                          String secondPersonSingular, String secondPersonPlural,
672                                          String thirdPersonSingular, String thirdPersonPlural)
673    {
674        irregular.put(word, new String[]{firstPersonSingular, firstPersonPlural, secondPersonSingular, secondPersonPlural,
675                thirdPersonSingular, thirdPersonSingular, thirdPersonSingular, thirdPersonPlural, thirdPersonSingular, thirdPersonSingular,
676                thirdPersonPlural});
677    }
678
679    static {
680        learnIrregularWord("m", "'m", "'re", "'re", "'re", "'s", "'re");
681        learnIrregularWord("am", "am", "are", "are", "are", "is", "are");
682        learnIrregularWord("ve", "'ve", "'ve", "'ve", "'ve", "'s", "'ve");
683        learnIrregularWord("have", "have", "have", "have", "have", "has", "have");
684        learnIrregularWord("haven_t", "haven't", "haven't", "haven't", "haven't", "hasn't", "haven't");
685        learnIrregularWord("do", "do", "do", "do", "do", "does", "do");
686        learnIrregularWord("don_t", "don't", "don't", "don't", "don't", "doesn't", "don't");
687        learnIrregularWord("this", "this", "these", "this", "these", "this", "these");
688    }
689
690    protected static class BeingSubstitution implements Substitution {
691
692        public String term;
693        public NounTrait trait;
694        public boolean finisher;
695        public BeingSubstitution()
696        {
697            term = "Joe";
698            trait = NounTrait.MALE_GENDER;
699            finisher = true;
700        }
701
702        public BeingSubstitution(String term, NounTrait trait, boolean finish)
703        {
704            this.term = (term == null) ? "Nullberoth of the North" : term;
705            this.trait = (trait == null) ? NounTrait.UNSPECIFIED_GENDER : trait;
706            finisher = finish;
707        }
708        public BeingSubstitution(NounTrait firstTrait, boolean finish, String... terms) {
709            int len;
710            if (terms == null || (len = terms.length) <= 0) {
711                term = "Nihilatia of Voidetica";
712                trait = (firstTrait == null) ? NounTrait.UNSPECIFIED_GENDER : firstTrait;
713                finisher = finish;
714            } else if (len == 1) {
715                term = (terms[0] == null) ? "Nullberoth of the North" : terms[0];
716                trait = NounTrait.UNSPECIFIED_GENDER;
717                finisher = finish;
718            } else if (len == 2) {
719                term = terms[0] + " and " + terms[1];
720                if (firstTrait == null)
721                    trait = NounTrait.GROUP;
722                else {
723                    switch (firstTrait) {
724                        case FIRST_PERSON_PLURAL:
725                        case FIRST_PERSON_SINGULAR:
726                            trait = NounTrait.FIRST_PERSON_PLURAL;
727                            break;
728                        case SECOND_PERSON_PLURAL:
729                        case SECOND_PERSON_SINGULAR:
730                            trait = NounTrait.SECOND_PERSON_PLURAL;
731                            break;
732                        default:
733                            trait = NounTrait.GROUP;
734                    }
735                }
736                finisher = finish;
737            } else {
738                StringBuilder sb = new StringBuilder().append(terms[0]).append(", ");
739                for (int i = 1; i < len - 1; i++) {
740                    sb.append(terms[i]).append(", ");
741                }
742                term = sb.append("and ").append(terms[len - 1]).toString();
743                if (firstTrait == null)
744                    trait = NounTrait.GROUP;
745                else {
746                    switch (firstTrait) {
747                        case FIRST_PERSON_PLURAL:
748                        case FIRST_PERSON_SINGULAR:
749                            trait = NounTrait.FIRST_PERSON_PLURAL;
750                            break;
751                        case SECOND_PERSON_PLURAL:
752                        case SECOND_PERSON_SINGULAR:
753                            trait = NounTrait.SECOND_PERSON_PLURAL;
754                            break;
755                        default:
756                            trait = NounTrait.GROUP;
757                    }
758                }
759                finisher = finish;
760
761            }
762        }
763        public static void appendCapitalized(String s, TextBuffer dest)
764        {
765            dest.append(Character.toUpperCase(s.charAt(0)));
766            if(s.length() > 1)
767                dest.append(s.substring(1));
768        }
769        @Override
770        public void appendSubstitution(MatchResult match, TextBuffer dest) {
771            if(match.isCaptured("at_sign"))
772            {
773                dest.append(finisher ? "@" : "\\@");
774            }
775            else if(match.isCaptured("caret_sign"))
776            {
777                dest.append(finisher ? "^" : "\\^");
778            }
779            else if(match.isCaptured("dollar_sign"))
780            {
781                dest.append(finisher ? "$" : "\\$");
782            }
783            else if(match.isCaptured("tilde_sign"))
784            {
785                dest.append(finisher ? "~" : "\\~");
786            }
787            else if(match.isCaptured("name"))
788            {
789                dest.append(trait.nameText(term));
790            }
791            else if(match.isCaptured("Name"))
792            {
793                appendCapitalized(trait.nameText(term), dest);
794            }
795            else if(match.isCaptured("name_s"))
796            {
797                dest.append(trait.name_sText(term));
798            }
799            else if(match.isCaptured("Name_s"))
800            {
801                appendCapitalized(trait.name_sText(term), dest);
802            }
803            else if(match.isCaptured("i"))
804            {
805                dest.append(trait.iText());
806            }
807            else if(match.isCaptured("I"))
808            {
809                appendCapitalized(trait.iText(), dest);
810            }
811            else if(match.isCaptured("me"))
812            {
813                dest.append(trait.meText());
814            }
815            else if(match.isCaptured("Me"))
816            {
817                appendCapitalized(trait.meText(), dest);
818            }
819            else if(match.isCaptured("my"))
820            {
821                dest.append(trait.myText());
822            }
823            else if(match.isCaptured("My"))
824            {
825                appendCapitalized(trait.myText(), dest);
826            }
827            else if(match.isCaptured("mine"))
828            {
829                dest.append(trait.mineText());
830            }
831            else if(match.isCaptured("Mine"))
832            {
833                appendCapitalized(trait.mineText(), dest);
834            }
835            else if(match.isCaptured("myself"))
836            {
837                dest.append(trait.myselfText());
838            }
839            else if(match.isCaptured("Myself"))
840            {
841                appendCapitalized(trait.myselfText(), dest);
842            }
843            else if(match.isCaptured("s"))
844            {
845                dest.append(trait.sText());
846            }
847            else if(match.isCaptured("ss"))
848            {
849                dest.append(trait.ssText());
850            }
851            else if(match.isCaptured("sss"))
852            {
853                dest.append(trait.sssText());
854            }
855            else if(match.isCaptured("usi"))
856            {
857                dest.append(trait.usiText());
858            }
859            else if(match.isCaptured("fves"))
860            {
861                dest.append(trait.fvesText());
862            }
863            else if(match.isCaptured("$"))
864            {
865                dest.append(trait.$Text());
866            }
867            else if(match.isCaptured("$$"))
868            {
869                dest.append(trait.$$Text());
870            }
871            else if(match.isCaptured("$$$"))
872            {
873                dest.append(trait.$$$Text());
874            }
875            else if(match.isCaptured("other"))
876            {
877                String[] others = irregular.get(match.group("other"));
878                if(others != null && others.length == 11)
879                    dest.append(others[trait.ordinal()]);
880                else
881                    match.getGroup(0, dest);
882            }
883            else if(match.isCaptured("Other"))
884            {
885                String[] others = irregular.get(match.group("Other").toLowerCase());
886                if(others != null && others.length == 11)
887                    appendCapitalized(others[trait.ordinal()], dest);
888                else
889                    match.getGroup(0, dest);
890            }
891            else if(match.isCaptured("direct"))
892            {
893                dest.append(trait.directText(term));
894            }
895            else if(match.isCaptured("Direct"))
896            {
897                appendCapitalized(trait.directText(term), dest);
898            }
899            else
900                match.getGroup(0, dest);
901        }
902    }
903}