// Package piglatin ports the classic "Pig Latin" translator — a staple Go // beginner exercise — to gno.land as a reusable pure package. // // Rules implemented (the standard English game): // - A word that starts with a vowel gets "way" appended: "apple" -> "appleway" // - A word that starts with one or more consonants has that leading // consonant cluster moved to the end, followed by "ay": "string" -> "ingstray" // - "y" acts as a consonant only when it is the first letter of the word // ("yellow" -> "ellowyay"); elsewhere it counts as a vowel ("myth" -> "ythmay"). // - Original capitalization of the word is preserved (title-case in, title-case // out): "Hello" -> "Ellohay". // - Trailing/leading punctuation attached to a word is preserved in place: // "Hello," -> "Ellohay,". // // Everything is pure strings/unicode — deterministic and reproducible on-chain. // // A live demo of this package (an interactive sentence translator) is at // [r/moul/x/daily/piglatindemo](/r/moul/x/daily/piglatindemo/v0). package piglatin import ( "strings" "unicode" ) const suffixVowel = "way" const suffixConsonant = "ay" func isVowel(r rune) bool { switch unicode.ToLower(r) { case 'a', 'e', 'i', 'o', 'u': return true } return false } // isLetter reports whether r is an ASCII/unicode letter (word character). func isLetter(r rune) bool { return unicode.IsLetter(r) } // translateWord converts a single "core" alphabetic word (no surrounding // punctuation) to Pig Latin, preserving its capitalization pattern. func translateWord(word string) string { if word == "" { return word } runes := []rune(word) // Find the leading consonant cluster. 'y' is a consonant only in position 0. start := 0 for i, r := range runes { if isVowel(r) { break } // 'y' after the first letter behaves like a vowel: stop the cluster. if i > 0 && unicode.ToLower(r) == 'y' { break } start = i + 1 } var out []rune if start == 0 { // Starts with a vowel. out = append(out, runes...) out = append(out, []rune(suffixVowel)...) } else if start >= len(runes) { // All consonants (no vowel found), e.g. "shh" — just append "ay". out = append(out, runes...) out = append(out, []rune(suffixConsonant)...) } else { out = append(out, runes[start:]...) out = append(out, runes[:start]...) out = append(out, []rune(suffixConsonant)...) } return applyCase(word, string(out)) } // applyCase re-applies the capitalization shape of the original word to the // translated word. Two common shapes are handled: ALL CAPS and Title-case; // everything else is returned lowercase. func applyCase(orig, translated string) string { origRunes := []rune(orig) if len(origRunes) == 0 { return translated } // Count letters and uppercase letters in the original. letters, uppers := 0, 0 for _, r := range origRunes { if unicode.IsLetter(r) { letters++ if unicode.IsUpper(r) { uppers++ } } } low := strings.ToLower(translated) switch { case letters > 0 && uppers == letters && letters > 1: // ALL CAPS -> keep upper. return strings.ToUpper(low) case unicode.IsUpper(origRunes[0]): // Title-case -> capitalize first letter of the result. tr := []rune(low) if len(tr) > 0 { tr[0] = unicode.ToUpper(tr[0]) } return string(tr) default: return low } } // splitAffixes separates a token into (leading punctuation, core word, // trailing punctuation) where the core is the contiguous run of letters // (apostrophes inside are kept as part of the core, e.g. "don't"). func splitAffixes(token string) (string, string, string) { runes := []rune(token) i := 0 for i < len(runes) && !isLetter(runes[i]) { i++ } j := len(runes) for j > i && !isLetter(runes[j-1]) { j-- } if i >= j { return token, "", "" // no letters at all } return string(runes[:i]), string(runes[i:j]), string(runes[j:]) } // TranslateToken translates a single whitespace-delimited token, preserving // any punctuation glued to its edges. func TranslateToken(token string) string { lead, core, trail := splitAffixes(token) if core == "" { return token } return lead + translateWord(core) + trail } // Translate applies Pig Latin to every word in the sentence while preserving // the original whitespace between words. func Translate(sentence string) string { var b strings.Builder var word strings.Builder flush := func() { if word.Len() > 0 { b.WriteString(TranslateToken(word.String())) word.Reset() } } for _, r := range sentence { if unicode.IsSpace(r) { flush() b.WriteRune(r) continue } word.WriteRune(r) } flush() return b.String() }