piglatin.gno
4.56 Kb · 168 lines
1// Package piglatin ports the classic "Pig Latin" translator — a staple Go
2// 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 leading
7// 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 word
9// ("yellow" -> "ellowyay"); elsewhere it counts as a vowel ("myth" -> "ythmay").
10// - Original capitalization of the word is preserved (title-case in, title-case
11// 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 at
18// [r/moul/x/daily/piglatindemo](/r/moul/x/daily/piglatindemo/v0).
19package piglatin
20
21import (
22 "strings"
23 "unicode"
24)
25
26const suffixVowel = "way"
27const suffixConsonant = "ay"
28
29func isVowel(r rune) bool {
30 switch unicode.ToLower(r) {
31 case 'a', 'e', 'i', 'o', 'u':
32 return true
33 }
34 return false
35}
36
37// isLetter reports whether r is an ASCII/unicode letter (word character).
38func isLetter(r rune) bool {
39 return unicode.IsLetter(r)
40}
41
42// translateWord converts a single "core" alphabetic word (no surrounding
43// punctuation) to Pig Latin, preserving its capitalization pattern.
44func translateWord(word string) string {
45 if word == "" {
46 return word
47 }
48 runes := []rune(word)
49
50 // Find the leading consonant cluster. 'y' is a consonant only in position 0.
51 start := 0
52 for i, r := range runes {
53 if isVowel(r) {
54 break
55 }
56 // 'y' after the first letter behaves like a vowel: stop the cluster.
57 if i > 0 && unicode.ToLower(r) == 'y' {
58 break
59 }
60 start = i + 1
61 }
62
63 var out []rune
64 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 }
77
78 return applyCase(word, string(out))
79}
80
81// applyCase re-applies the capitalization shape of the original word to the
82// 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 translated
88 }
89
90 // Count letters and uppercase letters in the original.
91 letters, uppers := 0, 0
92 for _, r := range origRunes {
93 if unicode.IsLetter(r) {
94 letters++
95 if unicode.IsUpper(r) {
96 uppers++
97 }
98 }
99 }
100
101 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 low
115 }
116}
117
118// splitAffixes separates a token into (leading punctuation, core word,
119// trailing punctuation) where the core is the contiguous run of letters
120// (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 := 0
124 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 all
133 }
134 return string(runes[:i]), string(runes[i:j]), string(runes[j:])
135}
136
137// TranslateToken translates a single whitespace-delimited token, preserving
138// any punctuation glued to its edges.
139func TranslateToken(token string) string {
140 lead, core, trail := splitAffixes(token)
141 if core == "" {
142 return token
143 }
144 return lead + translateWord(core) + trail
145}
146
147// Translate applies Pig Latin to every word in the sentence while preserving
148// the original whitespace between words.
149func Translate(sentence string) string {
150 var b strings.Builder
151 var word strings.Builder
152 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 continue
163 }
164 word.WriteRune(r)
165 }
166 flush()
167 return b.String()
168}