1// Package piglatin ports the classic "Pig Latin" translator — a staple Go2// beginner exercise — to gno.land as a reusable pure package.3//4// Rules implemented (the standard English game):5// - A word that starts with a vowel gets "way" appended: "apple" -> "appleway"6// - A word that starts with one or more consonants has that leading7// consonant cluster moved to the end, followed by "ay": "string" -> "ingstray"8// - "y" acts as a consonant only when it is the first letter of the word9// ("yellow" -> "ellowyay"); elsewhere it counts as a vowel ("myth" -> "ythmay").10// - Original capitalization of the word is preserved (title-case in, title-case11// out): "Hello" -> "Ellohay".12// - Trailing/leading punctuation attached to a word is preserved in place:13// "Hello," -> "Ellohay,".14//15// Everything is pure strings/unicode — deterministic and reproducible on-chain.16//17// A live demo of this package (an interactive sentence translator) is at18// [r/moul/x/daily/piglatindemo](/r/moul/x/daily/piglatindemo/v0).19package piglatin2021import (22 "strings"23 "unicode"24)2526const suffixVowel = "way"27const suffixConsonant = "ay"2829func isVowel(r rune) bool {30 switch unicode.ToLower(r) {31 case 'a', 'e', 'i', 'o', 'u':32 return true33 }34 return false35}3637// isLetter reports whether r is an ASCII/unicode letter (word character).38func isLetter(r rune) bool {39 return unicode.IsLetter(r)40}4142// translateWord converts a single "core" alphabetic word (no surrounding43// punctuation) to Pig Latin, preserving its capitalization pattern.44func translateWord(word string) string {45 if word == "" {46 return word47 }48 runes := []rune(word)4950 // Find the leading consonant cluster. 'y' is a consonant only in position 0.51 start := 052 for i, r := range runes {53 if isVowel(r) {54 break55 }56 // 'y' after the first letter behaves like a vowel: stop the cluster.57 if i > 0 && unicode.ToLower(r) == 'y' {58 break59 }60 start = i + 161 }6263 var out []rune64 if start == 0 {65 // Starts with a vowel.66 out = append(out, runes...)67 out = append(out, []rune(suffixVowel)...)68 } else if start >= len(runes) {69 // All consonants (no vowel found), e.g. "shh" — just append "ay".70 out = append(out, runes...)71 out = append(out, []rune(suffixConsonant)...)72 } else {73 out = append(out, runes[start:]...)74 out = append(out, runes[:start]...)75 out = append(out, []rune(suffixConsonant)...)76 }7778 return applyCase(word, string(out))79}8081// applyCase re-applies the capitalization shape of the original word to the82// translated word. Two common shapes are handled: ALL CAPS and Title-case;83// everything else is returned lowercase.84func applyCase(orig, translated string) string {85 origRunes := []rune(orig)86 if len(origRunes) == 0 {87 return translated88 }8990 // Count letters and uppercase letters in the original.91 letters, uppers := 0, 092 for _, r := range origRunes {93 if unicode.IsLetter(r) {94 letters++95 if unicode.IsUpper(r) {96 uppers++97 }98 }99 }100101 low := strings.ToLower(translated)102 switch {103 case letters > 0 && uppers == letters && letters > 1:104 // ALL CAPS -> keep upper.105 return strings.ToUpper(low)106 case unicode.IsUpper(origRunes[0]):107 // Title-case -> capitalize first letter of the result.108 tr := []rune(low)109 if len(tr) > 0 {110 tr[0] = unicode.ToUpper(tr[0])111 }112 return string(tr)113 default:114 return low115 }116}117118// splitAffixes separates a token into (leading punctuation, core word,119// trailing punctuation) where the core is the contiguous run of letters120// (apostrophes inside are kept as part of the core, e.g. "don't").121func splitAffixes(token string) (string, string, string) {122 runes := []rune(token)123 i := 0124 for i < len(runes) && !isLetter(runes[i]) {125 i++126 }127 j := len(runes)128 for j > i && !isLetter(runes[j-1]) {129 j--130 }131 if i >= j {132 return token, "", "" // no letters at all133 }134 return string(runes[:i]), string(runes[i:j]), string(runes[j:])135}136137// TranslateToken translates a single whitespace-delimited token, preserving138// any punctuation glued to its edges.139func TranslateToken(token string) string {140 lead, core, trail := splitAffixes(token)141 if core == "" {142 return token143 }144 return lead + translateWord(core) + trail145}146147// Translate applies Pig Latin to every word in the sentence while preserving148// the original whitespace between words.149func Translate(sentence string) string {150 var b strings.Builder151 var word strings.Builder152 flush := func() {153 if word.Len() > 0 {154 b.WriteString(TranslateToken(word.String()))155 word.Reset()156 }157 }158 for _, r := range sentence {159 if unicode.IsSpace(r) {160 flush()161 b.WriteRune(r)162 continue163 }164 word.WriteRune(r)165 }166 flush()167 return b.String()168}169Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.