piglatin.gno
6.53 Kb · 215 lines
1// Package piglatin ports the classic "Pig Latin" translator — a staple Go
2// beginner exercise — to an on-chain gno.land realm.
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.
16package piglatin
17
18import (
19 "strings"
20 "unicode"
21)
22
23const suffixVowel = "way"
24const suffixConsonant = "ay"
25
26func isVowel(r rune) bool {
27 switch unicode.ToLower(r) {
28 case 'a', 'e', 'i', 'o', 'u':
29 return true
30 }
31 return false
32}
33
34// isLetter reports whether r is an ASCII/unicode letter (word character).
35func isLetter(r rune) bool {
36 return unicode.IsLetter(r)
37}
38
39// translateWord converts a single "core" alphabetic word (no surrounding
40// punctuation) to Pig Latin, preserving its capitalization pattern.
41func translateWord(word string) string {
42 if word == "" {
43 return word
44 }
45 runes := []rune(word)
46
47 // Find the leading consonant cluster. 'y' is a consonant only in position 0.
48 start := 0
49 for i, r := range runes {
50 if isVowel(r) {
51 break
52 }
53 // 'y' after the first letter behaves like a vowel: stop the cluster.
54 if i > 0 && unicode.ToLower(r) == 'y' {
55 break
56 }
57 start = i + 1
58 }
59
60 var out []rune
61 if start == 0 {
62 // Starts with a vowel.
63 out = append(out, runes...)
64 out = append(out, []rune(suffixVowel)...)
65 } else if start >= len(runes) {
66 // All consonants (no vowel found), e.g. "shh" — just append "ay".
67 out = append(out, runes...)
68 out = append(out, []rune(suffixConsonant)...)
69 } else {
70 out = append(out, runes[start:]...)
71 out = append(out, runes[:start]...)
72 out = append(out, []rune(suffixConsonant)...)
73 }
74
75 return applyCase(word, string(out))
76}
77
78// applyCase re-applies the capitalization shape of the original word to the
79// translated word. Two common shapes are handled: ALL CAPS and Title-case;
80// everything else is returned lowercase.
81func applyCase(orig, translated string) string {
82 origRunes := []rune(orig)
83 if len(origRunes) == 0 {
84 return translated
85 }
86
87 // Count letters and uppercase letters in the original.
88 letters, uppers := 0, 0
89 for _, r := range origRunes {
90 if unicode.IsLetter(r) {
91 letters++
92 if unicode.IsUpper(r) {
93 uppers++
94 }
95 }
96 }
97
98 low := strings.ToLower(translated)
99 switch {
100 case letters > 0 && uppers == letters && letters > 1:
101 // ALL CAPS -> keep upper.
102 return strings.ToUpper(low)
103 case unicode.IsUpper(origRunes[0]):
104 // Title-case -> capitalize first letter of the result.
105 tr := []rune(low)
106 if len(tr) > 0 {
107 tr[0] = unicode.ToUpper(tr[0])
108 }
109 return string(tr)
110 default:
111 return low
112 }
113}
114
115// splitAffixes separates a token into (leading punctuation, core word,
116// trailing punctuation) where the core is the contiguous run of letters
117// (apostrophes inside are kept as part of the core, e.g. "don't").
118func splitAffixes(token string) (string, string, string) {
119 runes := []rune(token)
120 i := 0
121 for i < len(runes) && !isLetter(runes[i]) {
122 i++
123 }
124 j := len(runes)
125 for j > i && !isLetter(runes[j-1]) {
126 j--
127 }
128 if i >= j {
129 return token, "", "" // no letters at all
130 }
131 return string(runes[:i]), string(runes[i:j]), string(runes[j:])
132}
133
134// TranslateToken translates a single whitespace-delimited token, preserving
135// any punctuation glued to its edges.
136func TranslateToken(token string) string {
137 lead, core, trail := splitAffixes(token)
138 if core == "" {
139 return token
140 }
141 return lead + translateWord(core) + trail
142}
143
144// Translate applies Pig Latin to every word in the sentence while preserving
145// the original whitespace between words.
146func Translate(sentence string) string {
147 var b strings.Builder
148 var word strings.Builder
149 flush := func() {
150 if word.Len() > 0 {
151 b.WriteString(TranslateToken(word.String()))
152 word.Reset()
153 }
154 }
155 for _, r := range sentence {
156 if unicode.IsSpace(r) {
157 flush()
158 b.WriteRune(r)
159 continue
160 }
161 word.WriteRune(r)
162 }
163 flush()
164 return b.String()
165}
166
167// Render is the gnoweb entrypoint (Markdown). Root explains the rules; any
168// other path is treated as a sentence to translate, e.g. Render("/Hello world").
169func Render(path string) string {
170 var b strings.Builder
171
172 sentence := strings.TrimPrefix(path, "/")
173 sentence = strings.TrimSpace(sentence)
174
175 if sentence == "" {
176 b.WriteString("# Pig Latin\n\n")
177 b.WriteString("A tiny on-chain port of the classic **Pig Latin** translator ")
178 b.WriteString("(the go-to beginner exercise). Pure `strings`/`unicode`, fully deterministic.\n\n")
179 b.WriteString("## Rules\n\n")
180 b.WriteString("1. Word starts with a **vowel** → append `way`. `apple` → `appleway`\n")
181 b.WriteString("2. Word starts with **consonant(s)** → move the leading consonant cluster to the end, then add `ay`. `string` → `ingstray`\n")
182 b.WriteString("3. `y` is a consonant only as the first letter (`yellow` → `ellowyay`); otherwise a vowel (`myth` → `ythmay`).\n")
183 b.WriteString("4. **Capitalization** is preserved: `Hello` → `Ellohay`, `NASA` → `ASANAY`.\n")
184 b.WriteString("5. **Punctuation** glued to a word stays put: `Hello,` → `Ellohay,`.\n\n")
185 b.WriteString("## Try it\n\n")
186 b.WriteString("Append a sentence to the path — spaces are fine:\n\n")
187 for _, ex := range []string{"hello world", "The quick brown fox", "I love Gno!"} {
188 b.WriteString("- [`/")
189 b.WriteString(ex)
190 b.WriteString("`](/r/moul/x/daily/piglatin/v1:/")
191 b.WriteString(ex)
192 b.WriteString(") → `")
193 b.WriteString(Translate(ex))
194 b.WriteString("`\n")
195 }
196 b.WriteString("\n## Examples\n\n")
197 b.WriteString("| Input | Pig Latin |\n|---|---|\n")
198 for _, w := range []string{"pig", "banana", "smile", "eat", "yellow", "myth", "Hello,", "GNO"} {
199 b.WriteString("| `")
200 b.WriteString(w)
201 b.WriteString("` | `")
202 b.WriteString(Translate(w))
203 b.WriteString("` |\n")
204 }
205 return b.String()
206 }
207
208 b.WriteString("# Pig Latin\n\n")
209 b.WriteString("**Original:**\n\n> ")
210 b.WriteString(sentence)
211 b.WriteString("\n\n**Pig Latin:**\n\n> ")
212 b.WriteString(Translate(sentence))
213 b.WriteString("\n\n---\n\n[← rules & examples](/r/moul/x/daily/piglatin/v1)\n")
214 return b.String()
215}