Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}