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

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}