zbasu la jvozba iau ui
This commit is contained in:
parent
960291d567
commit
3633444ef1
|
@ -0,0 +1,7 @@
|
|||
# jvozba
|
||||
|
||||
An O(*n*) implementation of the lujvo-making algorithm to save the world.
|
||||
|
||||
## Usage
|
||||
|
||||
`main.go` should give you an idea of how to use the (extremely simple) basic API. If you want to customise stuff, dig into the code and you should find the right procedures to call.
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,277 @@
|
|||
package jvozba
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Score(lujvo string) int {
|
||||
L := len(lujvo)
|
||||
// apostrophe count, hyphen count, rafsi score count, vowel count
|
||||
A := 0
|
||||
H := 0
|
||||
R := 0
|
||||
V := 0
|
||||
var curr string
|
||||
for len(lujvo) > 0 {
|
||||
curr, lujvo = katna(lujvo)
|
||||
tai := rafsiTarmi(curr)
|
||||
R += int(tai)
|
||||
switch tai {
|
||||
case Hyphen:
|
||||
H++
|
||||
case CVhV:
|
||||
V += 2
|
||||
A++
|
||||
case CVCCV, CCVCV, CVV:
|
||||
V += 2
|
||||
default:
|
||||
V += 1
|
||||
}
|
||||
}
|
||||
return 1000*L - 500*A + 100*H - 10*R - V
|
||||
}
|
||||
|
||||
// the tarmi enum directly corresponds to the scores for R
|
||||
type tarmi int
|
||||
|
||||
const (
|
||||
Hyphen tarmi = iota
|
||||
CVCCV
|
||||
CVCC
|
||||
CCVCV
|
||||
CCVC
|
||||
CVC
|
||||
CVhV
|
||||
CCV
|
||||
CVV
|
||||
)
|
||||
|
||||
func rafsiTarmi(rafsi string) tarmi {
|
||||
l := len(rafsi)
|
||||
switch l {
|
||||
case 1:
|
||||
return Hyphen
|
||||
case 5:
|
||||
if isVowel(rafsi[2]) {
|
||||
return CCVCV
|
||||
} else {
|
||||
return CVCCV
|
||||
}
|
||||
case 4:
|
||||
if rafsi[2] == '\'' {
|
||||
return CVhV
|
||||
} else if !isVowel(rafsi[3]) {
|
||||
if isVowel(rafsi[2]) {
|
||||
return CCVC
|
||||
} else {
|
||||
return CVCC
|
||||
}
|
||||
} else {
|
||||
return Hyphen
|
||||
}
|
||||
// case 3:
|
||||
default:
|
||||
if !isVowel(rafsi[2]) {
|
||||
return CVC
|
||||
} else {
|
||||
if isVowel(rafsi[1]) {
|
||||
return CVV
|
||||
} else {
|
||||
return CCV
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type zunsna int
|
||||
|
||||
const (
|
||||
Unvoiced zunsna = iota
|
||||
Voiced
|
||||
Liquid
|
||||
)
|
||||
|
||||
func zunsnaType(one byte) zunsna {
|
||||
switch one {
|
||||
case 'b', 'd', 'g', 'v', 'j', 'z':
|
||||
return Voiced
|
||||
case 'p', 't', 'k', 'f', 'c', 's', 'x':
|
||||
return Unvoiced
|
||||
default:
|
||||
return Liquid
|
||||
}
|
||||
}
|
||||
|
||||
func isCjsz(one byte) bool {
|
||||
switch one {
|
||||
case 'c', 'j', 's', 'z':
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// `needsY` checks if an y-hyphen needs to be inserted
|
||||
func needsY(previous byte, current string) bool {
|
||||
if isVowel(previous) {
|
||||
return false
|
||||
}
|
||||
head := current[0]
|
||||
prevType := zunsnaType(previous)
|
||||
headType := zunsnaType(head)
|
||||
if (prevType == Voiced && headType == Unvoiced) || (headType == Voiced && prevType == Unvoiced) || previous == head || (isCjsz(previous) && isCjsz(head)) {
|
||||
return true
|
||||
}
|
||||
comb := string([]byte{previous, head})
|
||||
switch comb {
|
||||
case "cx", "kx", "xc", "xk", "mz":
|
||||
return true
|
||||
}
|
||||
switch string(previous) + current {
|
||||
case "ndj", "ndz", "ntc", "nts":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isValidInitial(twoBytes ...byte) bool {
|
||||
return len(twoBytes) == 2 &&
|
||||
strings.Contains("bl~br~cf~ck~cl~cm~cn~cp~cr~ct~dj~dr~dz~fl~fr~gl~gr~jb~jd~jg~jm~jv~kl~kr~ml~mr~pl~pr~sf~sk~sl~sm~sn~sp~sr~st~tc~tr~ts~vl~vr~xl~xr~zb~zd~zg~zm~zv", string(twoBytes))
|
||||
}
|
||||
|
||||
// Whether `lujvo` could lead to tosmabru on appending -y or is one already.
|
||||
func isTosmabruInitial(lujvo string) bool {
|
||||
var r string
|
||||
var lastChar byte
|
||||
i := 0
|
||||
for lujvo != "" {
|
||||
r, lujvo = katna(lujvo)
|
||||
switch t := rafsiTarmi(r); t {
|
||||
case CVC:
|
||||
if i > 0 && !isValidInitial(lastChar, r[0]) {
|
||||
return false
|
||||
}
|
||||
lastChar = r[2]
|
||||
case CVCCV:
|
||||
return i > 0 &&
|
||||
isValidInitial(lastChar, r[0]) &&
|
||||
isValidInitial(r[2], r[3])
|
||||
case Hyphen:
|
||||
return i > 1 && r == "y"
|
||||
default:
|
||||
return false
|
||||
}
|
||||
i++
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Mind the lack of y.
|
||||
func isVowel(one byte) bool {
|
||||
switch one {
|
||||
case 'a', 'e', 'i', 'o', 'u':
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Make one cut. Atrocious code.
|
||||
func katna(lujvo string) (string, string) {
|
||||
var point /* of cission */ int
|
||||
l := len(lujvo)
|
||||
switch {
|
||||
case l >= 4 && (lujvo[0] == 'n' || lujvo[0] == 'r' || lujvo[0] == 'y') && !isVowel(lujvo[1]):
|
||||
point = 1
|
||||
case l >= 8 && lujvo[4] == 'y':
|
||||
point = 4
|
||||
case l >= 7 && lujvo[3] == 'y':
|
||||
point = 3
|
||||
case l >= 7 && lujvo[2] == '\'' && isVowel(lujvo[3]):
|
||||
point = 4
|
||||
case l >= 6:
|
||||
point = 3
|
||||
default:
|
||||
point = l
|
||||
}
|
||||
return lujvo[:point], lujvo[point:]
|
||||
}
|
||||
|
||||
type scored struct {
|
||||
lujvo string
|
||||
score int
|
||||
tosmabru bool
|
||||
}
|
||||
|
||||
// -y-: L += 1 && H += 1 -> score += 1100
|
||||
const yPenalty = 1100
|
||||
|
||||
func Lujvo(selci [][]string) (string, error) {
|
||||
candidates := []scored{{"", 0, false}}
|
||||
for selciN, cnino := range selci {
|
||||
isLast := selciN == len(selci)-1
|
||||
newCand := []scored{}
|
||||
for _, rafsi := range cnino {
|
||||
var best *scored
|
||||
var bestTosmabru *scored
|
||||
for _, laldo := range candidates {
|
||||
l := len(laldo.lujvo)
|
||||
hyphen := ""
|
||||
if l > 0 && needsY(laldo.lujvo[l-1], rafsi[:2]) {
|
||||
hyphen = "y"
|
||||
} else if selciN == 1 {
|
||||
tai := rafsiTarmi(laldo.lujvo)
|
||||
if (tai == CVV || tai == CVhV) && !(isLast && rafsiTarmi(rafsi) == CCV) {
|
||||
if rafsi[0] == 'r' {
|
||||
hyphen = "n"
|
||||
} else {
|
||||
hyphen = "r"
|
||||
}
|
||||
}
|
||||
}
|
||||
if !isLast && (rafsiTarmi(rafsi) == CVCC || rafsiTarmi(rafsi) == CCVC) {
|
||||
rafsi += "y"
|
||||
}
|
||||
newPart := hyphen + rafsi
|
||||
newLujvo := laldo.lujvo + newPart
|
||||
newScore := laldo.score + Score(newPart)
|
||||
tosmabru := isTosmabruInitial(newLujvo)
|
||||
if laldo.tosmabru {
|
||||
newScore -= yPenalty
|
||||
}
|
||||
if tosmabru {
|
||||
newScore += yPenalty
|
||||
}
|
||||
newScored := scored{newLujvo, newScore, tosmabru}
|
||||
// DRY
|
||||
if tosmabru {
|
||||
if bestTosmabru == nil || bestTosmabru.score > newScore {
|
||||
bestTosmabru = &newScored
|
||||
}
|
||||
} else {
|
||||
if best == nil || best.score > newScore {
|
||||
best = &newScored
|
||||
}
|
||||
}
|
||||
}
|
||||
if best != nil {
|
||||
newCand = append(newCand, *best)
|
||||
}
|
||||
if bestTosmabru != nil {
|
||||
newCand = append(newCand, *bestTosmabru)
|
||||
}
|
||||
}
|
||||
candidates = newCand
|
||||
}
|
||||
bestOption := candidates[0]
|
||||
for _, o := range candidates {
|
||||
if bestOption.score > o.score {
|
||||
bestOption = o
|
||||
}
|
||||
}
|
||||
result := bestOption.lujvo
|
||||
if bestOption.tosmabru {
|
||||
result = result[:3] + "y" + result[3:]
|
||||
}
|
||||
return result, nil
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
package jvozba
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHyphenation(t *testing.T) {
|
||||
examples := []struct {
|
||||
whole string
|
||||
parts string
|
||||
types string
|
||||
}{
|
||||
{"zbasai", "zba-sai", "78"},
|
||||
{"nunynau", "nun-y-nau", "508"},
|
||||
{"sairzbata'u", "sai-r-zba-ta'u", "8076"},
|
||||
{"zbazbasysarji", "zba-zbas-y-sarji", "7401"}}
|
||||
for i, e := range examples {
|
||||
total := []string{}
|
||||
types := ""
|
||||
s := e.whole
|
||||
var sle string
|
||||
for len(s) > 0 {
|
||||
sle, s = katna(s)
|
||||
total = append(total, sle)
|
||||
types += fmt.Sprintf("%d", int(rafsiTarmi(sle)))
|
||||
}
|
||||
attempt := strings.Join(total, "-")
|
||||
if e.parts != attempt || e.types != types {
|
||||
t.Errorf("'%s' (example #%d): got %s %s, expected %s %s",
|
||||
e.whole, i, attempt, types, e.parts, e.types)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// see https://lojban.github.io/cll/4/12/
|
||||
func TestCLLScoring(t *testing.T) {
|
||||
examples := []struct {
|
||||
string
|
||||
int
|
||||
}{
|
||||
{"zbasai", 5847},
|
||||
{"nunynau", 6967},
|
||||
{"sairzbata'u", 10385},
|
||||
{"zbazbasysarji", 12976}}
|
||||
for i, e := range examples {
|
||||
s := Score(e.string)
|
||||
if s != e.int {
|
||||
t.Errorf("'%s' (example #%d): got %d, expected %d",
|
||||
e.string, i, s, e.int)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// see https://lojban.github.io/cll/4/13/
|
||||
func TestCLLExamples(t *testing.T) {
|
||||
examples := []struct {
|
||||
selci [][]string
|
||||
lujvo string
|
||||
score int
|
||||
}{
|
||||
{[][]string{{"ger", "ge'u", "gerk"}, {"zda", "zdani"}}, "gerzda", 5878},
|
||||
{[][]string{{"lot", "blo", "lo'i"}, {"kle", "lei"}}, "blolei", 5847},
|
||||
{[][]string{{"loj", "logj"}, {"ban", "bau", "bang"}, {"gri", "girzu"}}, "lojbaugri", 8796},
|
||||
{[][]string{{"loj", "logj"}, {"ban", "bau", "bang"}, {"gir", "girz"}}, "lojbaugir", 8816},
|
||||
{[][]string{{"nak", "nakn"}, {"kem"}, {"cin", "cins"}, {"ctu", "ctuca"}}, "nakykemcinctu", 12876}}
|
||||
for i, e := range examples {
|
||||
s, err := Lujvo(e.selci)
|
||||
if err != nil {
|
||||
t.Errorf("(example #%d): error %v", i, err)
|
||||
continue
|
||||
}
|
||||
score := Score(s)
|
||||
if s != e.lujvo || score != e.score {
|
||||
t.Errorf("(example #%d): got %s (%d), expected %s (%d)",
|
||||
i, s, score, e.lujvo, e.score)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsTosmabruInitial(t *testing.T) {
|
||||
examples := map[string]bool{
|
||||
"": true,
|
||||
"jacnal": true,
|
||||
"jacnalsel": false,
|
||||
"skebap": false,
|
||||
"skebai": false,
|
||||
"pevrisn": false,
|
||||
}
|
||||
for k, v := range examples {
|
||||
res := isTosmabruInitial(k)
|
||||
if res != v {
|
||||
t.Errorf("'%s': got %v",
|
||||
k, res)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"jvozba"
|
||||
)
|
||||
|
||||
func main() {
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
for scanner.Scan() {
|
||||
in := scanner.Text()
|
||||
res, err := jvozba.Jvozba(in)
|
||||
if err != nil {
|
||||
fmt.Printf("got error: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("%s → %s (%d)\n", in, res, jvozba.Score(res))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
package jvozba
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func isGismu(what string) bool {
|
||||
return len(what) == 5 &&
|
||||
!isVowel(what[0]) &&
|
||||
!isVowel(what[3]) &&
|
||||
isVowel(what[4]) &&
|
||||
isVowel(what[1]) != isVowel(what[2])
|
||||
}
|
||||
|
||||
func selci(tanru string, rafste map[string][]string, isCmene bool) ([][]string, error) {
|
||||
parts := strings.Split(tanru, " ")
|
||||
count := 0
|
||||
selci := make([][]string, len(parts))
|
||||
for i, p := range parts {
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
r := rafste[p]
|
||||
if r == nil && !isGismu(p) {
|
||||
return [][]string{}, fmt.Errorf("no rafsi found for %s", p)
|
||||
}
|
||||
if isGismu(p) {
|
||||
r = append(r, p, p[:4])
|
||||
}
|
||||
filtered := make([]string, len(r))
|
||||
c := 0
|
||||
for _, one := range r {
|
||||
var keep bool
|
||||
switch rafsiTarmi(one) {
|
||||
case CVCCV, CCVCV:
|
||||
keep = i == len(parts)-1 && !isCmene
|
||||
case CCV, CVV, CVhV:
|
||||
keep = !(i == len(parts)-1 && isCmene)
|
||||
case CVCC, CCVC, CVC:
|
||||
keep = !(i == len(parts)-1 && !isCmene)
|
||||
}
|
||||
if keep {
|
||||
filtered[c] = one
|
||||
c++
|
||||
}
|
||||
}
|
||||
if c == 0 {
|
||||
return [][]string{}, fmt.Errorf("no applicable rafsi found for %s", p)
|
||||
}
|
||||
selci[count] = filtered[:c]
|
||||
count++
|
||||
}
|
||||
return selci[:count], nil
|
||||
}
|
||||
|
||||
func Zbasu(tanru string, rafste map[string][]string, isCmene bool) (string, error) {
|
||||
slemei, err := selci(tanru, rafste, isCmene)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
res, err := Lujvo(slemei)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return res, nil
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package jvozba
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGeneration(t *testing.T) {
|
||||
type example struct {
|
||||
tanru string
|
||||
selci [][]string
|
||||
}
|
||||
examples := []example{
|
||||
example{"gerku zdani", [][]string{{"ge'u", "ger", "gerk"}, {"zda", "zdani"}}},
|
||||
example{"bloti klesi", [][]string{{"blo", "lo'i", "lot", "blot"}, {"kle", "lei", "klesi"}}},
|
||||
example{"logji bangu girzu", [][]string{{"loj", "logj"}, {"ban", "bau", "bang"}, {"gri", "girzu"}}},
|
||||
example{"nakni ke cinse ctuca", [][]string{{"nak", "nakn"}, {"kem"}, {"cin", "cins"}, {"ctu", "ctuca"}}}}
|
||||
for i, e := range examples {
|
||||
s, err := selci(e.tanru, Rafsi, false)
|
||||
if err != nil {
|
||||
t.Errorf("(example #%d): error %v", i, err)
|
||||
} else if fmt.Sprintf("%v", s) != fmt.Sprintf("%v", e.selci) {
|
||||
t.Errorf("(example #%d): got %v, expected %v", i, s, e.selci)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue