zbasu la jvozba iau ui

This commit is contained in:
ciuak 2019-06-15 22:21:54 +02:00
parent 960291d567
commit 3633444ef1
9 changed files with 6976 additions and 0 deletions

7
README.md Normal file
View File

@ -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.

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module github.com/ciuak/jvozba
go 1.12

1208
jvozba.go Normal file

File diff suppressed because it is too large Load Diff

5268
jvozba_test.go Normal file

File diff suppressed because it is too large Load Diff

277
lujvo.go Normal file
View File

@ -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
}

98
lujvo_test.go Normal file
View File

@ -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)
}
}
}

22
main.go Normal file
View File

@ -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))
}
}
}

67
zbasu.go Normal file
View File

@ -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
}

26
zbasu_test.go Normal file
View File

@ -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)
}
}
}