711 lines
16 KiB
Go
711 lines
16 KiB
Go
package hacker
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"math/rand"
|
|
"strings"
|
|
|
|
"github.com/zorchenhimer/hacker-quotes/database"
|
|
"github.com/zorchenhimer/hacker-quotes/models"
|
|
)
|
|
|
|
type InitialData struct {
|
|
Adjectives [][]string
|
|
Nouns [][]string
|
|
Verbs [][]string
|
|
Pronouns [][]string
|
|
Sentences []string
|
|
}
|
|
|
|
type english struct {
|
|
db database.DB
|
|
|
|
currentPronoun string
|
|
}
|
|
|
|
func NewEnglish(db database.DB) (HackerQuotes, error) {
|
|
return &english{db: db}, nil
|
|
}
|
|
|
|
/*
|
|
Sentence format
|
|
|
|
bare words
|
|
{word_type:options}
|
|
{{word_type:new word:properties}}
|
|
|
|
[rng:random,choice,list] // one word is chosen from the comma-separated list
|
|
|
|
// Randomly hide/show the enclosed definition
|
|
[hide:{word_type:options}]
|
|
[hide:{{word_type:new word:options}}]
|
|
|
|
{pronoun} can't {verb:i,present} {noun_phrase}, it {verb:it,future} {noun_phrase}!
|
|
{verb:you,present} {noun_phrase:indefinite}, then you can {verb:you,present} {noun_phrase:definite}!
|
|
{noun_phrase} {verb}. With {noun_phrase:indifinite,noadj,compound}!
|
|
|
|
{pronoun} {{verb:need:from_pronoun,present}} to {verb:i,present} {noun_phrase:definite}!
|
|
|
|
{pronoun} [rng:can,may] {verb:i,present} {noun_phrase} [hide:{noun_phrase:singlular}], it {verb:it,future} {noun_phrase:definite}.
|
|
*/
|
|
|
|
func (g *english) Hack() (string, error) {
|
|
//var fmtString string = `{verb:you,present} {noun_phrase:definite} then you can {verb:you,present} {noun_phrase:definite}!`
|
|
//var str string = `{pronoun} {{verb:need:from_pronoun,present}} to {verb:i,present} {noun_phrase:definite}!`
|
|
//var str string = `{pronoun} [rng:can,may] {verb:i,present} {noun_phrase}[hide: {noun_phrase:singlular}], it {verb:it,future} {noun_phrase:definite}.`
|
|
str, err := g.randomSentence()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return g.HackThis(str)
|
|
}
|
|
|
|
func (g *english) HackThis(fmtString string) (string, error) {
|
|
var idx int
|
|
var err error
|
|
var nidx int
|
|
output := &strings.Builder{}
|
|
g.currentPronoun = ""
|
|
|
|
for idx < len(fmtString) {
|
|
if fmtString[idx] == '{' {
|
|
nidx, err = g.consumeWord(fmtString, idx, output)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
idx = nidx
|
|
continue
|
|
|
|
} else if fmtString[idx] == '[' {
|
|
nidx, err = g.consumeRng(fmtString, idx, output)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
idx = nidx
|
|
continue
|
|
}
|
|
|
|
nidx, err = g.consumeRaw(fmtString, idx, output)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
idx = nidx
|
|
}
|
|
|
|
return toCap(output.String()), nil
|
|
}
|
|
|
|
func (g *english) consumeRng(fmtString string, idx int, output *strings.Builder) (int, error) {
|
|
idx++
|
|
def := strings.Index(fmtString[idx:], ":")
|
|
if def == -1 {
|
|
return 0, fmt.Errorf("Missing command separator in RNG block")
|
|
}
|
|
def += idx
|
|
|
|
end := strings.Index(fmtString[idx:], "]")
|
|
if end == -1 {
|
|
return 0, fmt.Errorf("Unclosed RNG block starting at offset %d", idx-1)
|
|
}
|
|
end += idx
|
|
|
|
if def > end {
|
|
return 0, fmt.Errorf("Missing command separator in RNG block")
|
|
}
|
|
|
|
switch fmtString[idx:def] {
|
|
case "rng":
|
|
choices := strings.Split(fmtString[def+1:end], ",")
|
|
ridx := rand.Intn(len(choices))
|
|
output.WriteString(choices[ridx])
|
|
|
|
case "hide":
|
|
if rand.Int() % 2 == 0 {
|
|
return end+1, nil
|
|
}
|
|
|
|
var newEnd int = def+1
|
|
var err error
|
|
for newEnd < end {
|
|
if fmtString[newEnd] == '{' {
|
|
newEnd, err = g.consumeWord(fmtString, newEnd, output)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
} else {
|
|
newEnd, err = g.consumeRaw(fmtString, newEnd, output)
|
|
}
|
|
}
|
|
|
|
default:
|
|
return 0, fmt.Errorf("RNG type %q is not implemented", fmtString[idx:def])
|
|
}
|
|
|
|
return end+1, nil
|
|
}
|
|
|
|
func (g *english) consumeRaw(fmtString string, idx int, output *strings.Builder) (int, error) {
|
|
end := strings.IndexAny(fmtString[idx:], "{[")
|
|
if end == -1 {
|
|
output.WriteString(fmtString[idx:len(fmtString)])
|
|
return len(fmtString), nil
|
|
}
|
|
|
|
output.WriteString(fmtString[idx:end+idx])
|
|
return idx+end, nil
|
|
}
|
|
|
|
func (g *english) parseVerbOptions(optlist []string) (models.ConjugationType, models.ConjugationTime, bool) {
|
|
var ct models.ConjugationType = models.ConjugationType(rand.Intn(5) + 1)
|
|
if sliceContains(optlist, "i") {
|
|
ct = models.CT_I
|
|
} else if sliceContains(optlist, "you") {
|
|
ct = models.CT_You
|
|
} else if sliceContains(optlist, "it") {
|
|
ct = models.CT_It
|
|
} else if sliceContains(optlist, "we") {
|
|
ct = models.CT_We
|
|
} else if sliceContains(optlist, "they") {
|
|
ct = models.CT_They
|
|
}
|
|
|
|
var cm models.ConjugationTime = models.ConjugationTime(rand.Intn(3) + 1)
|
|
if sliceContains(optlist, "present") {
|
|
cm = models.CM_Present
|
|
} else if sliceContains(optlist, "past") {
|
|
cm = models.CM_Past
|
|
} else if sliceContains(optlist, "future") {
|
|
cm = models.CM_Future
|
|
}
|
|
|
|
var invert bool = rand.Int() % 2 == 0
|
|
if sliceContains(optlist, "invert") {
|
|
invert = true
|
|
}
|
|
|
|
//var regular bool = true
|
|
//if sliceContains(optlist, "irregular") {
|
|
// regular = false
|
|
//}
|
|
|
|
if sliceContains(optlist, "from_pronoun") {
|
|
switch g.currentPronoun {
|
|
case "he", "she", "it":
|
|
ct = models.CT_It
|
|
case "i":
|
|
ct = models.CT_I
|
|
case "you":
|
|
ct = models.CT_You
|
|
case "we":
|
|
ct = models.CT_We
|
|
case "they":
|
|
ct = models.CT_They
|
|
}
|
|
}
|
|
|
|
return ct, cm, invert
|
|
}
|
|
|
|
func (g *english) parseNounOptions(optlist []string) (bool, bool) {
|
|
var plural bool = rand.Int() % 2 == 0
|
|
if sliceContains(optlist, "plural") {
|
|
plural = true
|
|
} else if sliceContains(optlist, "singular") {
|
|
plural = false
|
|
}
|
|
|
|
var compound bool = rand.Int() % 2 == 0
|
|
if sliceContains(optlist, "compound") {
|
|
compound = true
|
|
} else if sliceContains(optlist, "simple") {
|
|
compound = false
|
|
}
|
|
|
|
return plural, compound
|
|
}
|
|
|
|
func (g *english) consumeNewWord(fmtString string, idx int, output *strings.Builder) (int, error) {
|
|
idx += 2
|
|
var wordtype string
|
|
var word string
|
|
var options string
|
|
|
|
end := strings.Index(fmtString[idx:], "}")
|
|
if end == -1 {
|
|
return 0, fmt.Errorf("[consumeNewWord] Unclosed definition starting at %d", idx)
|
|
}
|
|
end += idx
|
|
|
|
// Find the start of the new word
|
|
def := strings.Index(fmtString[idx:], ":")
|
|
if def == -1 {
|
|
return 0, fmt.Errorf("[consumeNewWord] Word definition missing word")
|
|
}
|
|
def += idx
|
|
wordtype = fmtString[idx:def]
|
|
|
|
// Find the start of the options
|
|
opts := strings.Index(fmtString[def+1:], ":")
|
|
|
|
if opts > -1 {
|
|
opts += def+2
|
|
options = fmtString[opts:end]
|
|
word = fmtString[def+1:opts-1]
|
|
} else {
|
|
word = fmtString[def+1:end]
|
|
}
|
|
|
|
//fmt.Printf("idx:%d def:%d opts:%d end:%d type:%q word:%q opts:%q\n",
|
|
// idx, def, opts, end,
|
|
// wordtype,
|
|
// word,
|
|
// options)
|
|
|
|
optlist := strings.Split(options, ",")
|
|
var formatted string
|
|
switch wordtype {
|
|
case "verb":
|
|
ct, cm, invert := g.parseVerbOptions(optlist)
|
|
|
|
v := models.Verb{
|
|
Regular: true,
|
|
Word: word,
|
|
}
|
|
|
|
formatted = v.Conjugate(ct, cm, invert)
|
|
|
|
case "noun":
|
|
plural, _ := g.parseNounOptions(optlist)
|
|
|
|
if plural {
|
|
n := models.Noun{
|
|
Regular: true,
|
|
Word: word,
|
|
}
|
|
formatted = n.Plural()
|
|
} else {
|
|
formatted = word
|
|
}
|
|
|
|
default:
|
|
// Pronouns and adjectives aren't implemented because they have no modifications to apply.
|
|
return 0, fmt.Errorf("Word type %q not implemented creating in-sentence", wordtype)
|
|
}
|
|
|
|
output.WriteString(formatted)
|
|
return end+2, nil
|
|
}
|
|
|
|
func (g *english) consumeWord(fmtString string, idx int, output *strings.Builder) (int, error) {
|
|
if fmtString[idx+1] == '{' {
|
|
return g.consumeNewWord(fmtString, idx, output)
|
|
}
|
|
|
|
idx++
|
|
var wordtype string
|
|
var options string
|
|
|
|
end := strings.Index(fmtString[idx:], "}")
|
|
if end == -1 {
|
|
return 0, fmt.Errorf("[consumeWord] Unclosed definition starting at %d", idx)
|
|
}
|
|
|
|
end += idx
|
|
optsStart := strings.Index(fmtString[idx:end], ":")
|
|
|
|
if optsStart != -1 {
|
|
options = fmtString[optsStart+idx+1:end]
|
|
wordtype = fmtString[idx:optsStart+idx]
|
|
} else {
|
|
wordtype = fmtString[idx:end]
|
|
}
|
|
|
|
if wordtype == "" {
|
|
return 0, fmt.Errorf("[consumeWord] Missing word type at idx: %d", idx)
|
|
}
|
|
|
|
opts := strings.Split(options, ",")
|
|
var word string
|
|
var err error
|
|
|
|
switch wordtype {
|
|
case "pronoun":
|
|
var plural bool = rand.Int() % 2 == 0
|
|
if sliceContains(opts, "plural") {
|
|
plural = true
|
|
} else if sliceContains(opts, "singular") {
|
|
plural = false
|
|
}
|
|
|
|
word, err = g.randomPronoun(plural)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
g.currentPronoun = word
|
|
|
|
case "verb":
|
|
ct, cm, invert := g.parseVerbOptions(opts)
|
|
|
|
word, err = g.randomVerb(ct, cm, invert)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
case "noun":
|
|
plural, compound := g.parseNounOptions(opts)
|
|
|
|
word, err = g.randomNoun(plural, compound)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
case "noun_phrase":
|
|
var definite bool = rand.Int() % 2 == 0
|
|
var hasAdj bool = rand.Int() % 2 == 0
|
|
var plural bool = rand.Int() % 2 == 0
|
|
var compound bool = rand.Int() % 2 == 0
|
|
|
|
if sliceContains(opts, "indefinite") {
|
|
definite = false
|
|
} else if sliceContains(opts, "definite") {
|
|
definite = true
|
|
}
|
|
|
|
if sliceContains(opts, "noadj") {
|
|
hasAdj = false
|
|
} else if sliceContains(opts, "adj") {
|
|
hasAdj = true
|
|
}
|
|
|
|
if sliceContains(opts, "plural") {
|
|
plural = true
|
|
} else if sliceContains(opts, "singular") {
|
|
plural = false
|
|
}
|
|
|
|
if sliceContains(opts, "compound") {
|
|
compound = true
|
|
} else if sliceContains(opts, "simple") {
|
|
compound = false
|
|
}
|
|
|
|
word, err = g.nounPhrase(definite, hasAdj, plural, compound)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
case "adjective":
|
|
word, err = g.randomAdjective()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
default:
|
|
return 0, fmt.Errorf("[consumeWord] Invalid word type %s at %d", wordtype, idx)
|
|
}
|
|
|
|
output.WriteString(word)
|
|
return end+1, nil
|
|
}
|
|
|
|
func (g *english) nounPhrase(definite, hasAdj, plural, compound bool) (string, error){
|
|
adj := ""
|
|
var err error
|
|
if hasAdj {
|
|
adj, err = g.randomAdjective()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
noun, err := g.randomNoun(plural, compound)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
phrase := adj
|
|
if phrase != "" {
|
|
phrase += " " + noun
|
|
} else {
|
|
phrase = noun
|
|
}
|
|
|
|
if definite && !plural {
|
|
//fmt.Println("[nounPhrase] definite && !plural")
|
|
return "the " + phrase, nil
|
|
}
|
|
|
|
if !plural {
|
|
//fmt.Println("[nounPhrase] !plural")
|
|
return g.ana(phrase), nil
|
|
}
|
|
|
|
return phrase, nil
|
|
}
|
|
|
|
func (g *english) randomAdjective() (string, error) {
|
|
ids, err := g.db.GetAdjectiveIds()
|
|
if err != nil {
|
|
return "", fmt.Errorf("[adj] get IDs error: %v", err)
|
|
}
|
|
|
|
if len(ids) <= 0 {
|
|
return "", fmt.Errorf("No adjective IDs returned from database")
|
|
}
|
|
|
|
rid := int(rand.Int63n(int64(len(ids))))
|
|
//fmt.Printf("[adj] len(ids): %d; rid: %d; %d\n", len(ids), rid, ids[rid])
|
|
|
|
adj, err := g.db.GetAdjective(ids[rid])
|
|
if err != nil {
|
|
return "", fmt.Errorf("[adj] ID: %d; %v", ids[rid], err)
|
|
}
|
|
|
|
return adj.Word, nil
|
|
}
|
|
|
|
func (g *english) randomNoun(plural, compound bool) (string, error) {
|
|
var ids []int
|
|
var err error
|
|
if compound {
|
|
ids, err = g.db.GetNounIds(true, true, true)
|
|
if err != nil {
|
|
return "", fmt.Errorf("[noun] get IDs error: %v", err)
|
|
}
|
|
} else {
|
|
ids, err = g.db.GetNounIds(true, false, false)
|
|
if err != nil {
|
|
return "", fmt.Errorf("[noun] get IDs error: %v", err)
|
|
}
|
|
}
|
|
|
|
if len(ids) <= 0 {
|
|
return "", fmt.Errorf("No noun IDs returned from database")
|
|
}
|
|
|
|
rid := int(rand.Int63n(int64(len(ids))))
|
|
//fmt.Printf("[noun] len(ids): %d; rid: %d; ID: %d\n", len(ids), rid, ids[rid])
|
|
|
|
noun, err := g.db.GetNoun(ids[rid])
|
|
if err != nil {
|
|
return "", fmt.Errorf("[noun] ID: %d; %v", ids[rid], err)
|
|
}
|
|
|
|
if plural {
|
|
return noun.Plural(), nil
|
|
}
|
|
return noun.Word, nil
|
|
}
|
|
|
|
func (g *english) randomVerb(ctype models.ConjugationType, ctime models.ConjugationTime, invert bool) (string, error) {
|
|
ids, err := g.db.GetVerbIds()
|
|
if err != nil {
|
|
return "", fmt.Errorf("[verb] get IDs error: %v", err)
|
|
}
|
|
|
|
if len(ids) <= 0 {
|
|
return "", fmt.Errorf("No verb IDs returned from database")
|
|
}
|
|
|
|
rid := int(rand.Int63n(int64(len(ids))))
|
|
verb, err := g.db.GetVerb(ids[rid])
|
|
if err != nil {
|
|
return "", fmt.Errorf("[verb] ID: %d; %v", ids[rid], err)
|
|
}
|
|
|
|
return verb.Conjugate(ctype, ctime, invert), nil
|
|
}
|
|
|
|
func (g *english) randomPronoun(plural bool) (string, error) {
|
|
ids, err := g.db.GetPronounIds(plural)
|
|
if err != nil {
|
|
return "", fmt.Errorf("[pronoun] get IDs error: %v", err)
|
|
}
|
|
|
|
if len(ids) <= 0 {
|
|
return "", fmt.Errorf("No pronoun IDs returned from database")
|
|
}
|
|
|
|
rid := int(rand.Int63n(int64(len(ids))))
|
|
pronoun, err := g.db.GetPronoun(ids[rid])
|
|
if err != nil {
|
|
return "", fmt.Errorf("[pronoun] ID: %d; %v", ids[rid], err)
|
|
}
|
|
|
|
if pronoun.Word == "i" {
|
|
return strings.ToUpper(pronoun.Word), nil
|
|
}
|
|
return pronoun.Word, nil
|
|
}
|
|
|
|
func (g *english) randomSentence() (string, error) {
|
|
ids, err := g.db.GetSentenceIds()
|
|
if err != nil {
|
|
return "", fmt.Errorf("[sentence] get IDs error: %v", err)
|
|
}
|
|
|
|
if len(ids) <= 0 {
|
|
return "", fmt.Errorf("[sentence] No sentence IDs returned from database")
|
|
}
|
|
|
|
rid := int(rand.Int63n(int64(len(ids))))
|
|
sentence, err := g.db.GetSentence(ids[rid])
|
|
if err != nil {
|
|
return "", fmt.Errorf("[sentence] ID: %d, %v", ids[rid], err)
|
|
}
|
|
|
|
return sentence, nil
|
|
}
|
|
|
|
func (g *english) InitData(filename string) error {
|
|
fmt.Printf("Initializing database with data in %q\n", filename)
|
|
if g.db == nil {
|
|
return fmt.Errorf("databse is nil!")
|
|
}
|
|
|
|
raw, err := ioutil.ReadFile(filename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
//data := map[string][]interface{}{}
|
|
data := InitialData{}
|
|
if err = json.Unmarshal(raw, &data); err != nil {
|
|
return err
|
|
}
|
|
|
|
if data.Adjectives == nil || len(data.Adjectives) == 0 {
|
|
return fmt.Errorf("Missing Adjectives in input data")
|
|
}
|
|
|
|
adjectives := []models.Adjective{}
|
|
for _, adj := range data.Adjectives {
|
|
t, word := adj[0], adj[1]
|
|
a := models.Adjective{Word: word}
|
|
if strings.Contains(t, "a") {
|
|
a.Absolute = true
|
|
}
|
|
if strings.Contains(t, "e") {
|
|
a.AppendEst = true
|
|
}
|
|
if strings.Contains(t, "m") {
|
|
a.AppendMore = true
|
|
}
|
|
|
|
adjectives = append(adjectives, a)
|
|
}
|
|
|
|
if data.Nouns == nil || len(data.Nouns) == 0 {
|
|
return fmt.Errorf("Missing nouns key in data")
|
|
}
|
|
|
|
nouns := []models.Noun{}
|
|
for _, noun := range data.Nouns {
|
|
t, word := noun[0], noun[1]
|
|
n := models.Noun{Word: word}
|
|
|
|
if strings.Contains(t, "m") {
|
|
n.Multiple = true
|
|
}
|
|
|
|
if strings.Contains(t, "b") {
|
|
n.Begin = true
|
|
}
|
|
|
|
if strings.Contains(t, "e") {
|
|
n.End = true
|
|
}
|
|
|
|
if strings.Contains(t, "a") {
|
|
n.Alone = true
|
|
}
|
|
|
|
if strings.Contains(t, "r") {
|
|
n.Regular = true
|
|
}
|
|
|
|
nouns = append(nouns, n)
|
|
}
|
|
|
|
if data.Verbs == nil || len(data.Verbs) == 0 {
|
|
return fmt.Errorf("Missing verbs key in data")
|
|
}
|
|
|
|
verbs := []models.Verb{}
|
|
for _, verb := range data.Verbs {
|
|
v := models.Verb{Word: verb[1]}
|
|
if strings.Contains(verb[0], "r") {
|
|
v.Regular = true
|
|
}
|
|
|
|
verbs = append(verbs, v)
|
|
}
|
|
|
|
if data.Pronouns == nil || len(data.Pronouns) == 0 {
|
|
return fmt.Errorf("Missing pronouns key in data")
|
|
}
|
|
|
|
pronouns := []models.Pronoun{}
|
|
for _, pro := range data.Pronouns {
|
|
p := models.Pronoun{Word: pro[1]}
|
|
if strings.Contains(pro[0], "p") {
|
|
p.Plural = true
|
|
}
|
|
pronouns = append(pronouns, p)
|
|
}
|
|
|
|
if data.Sentences == nil || len(data.Sentences) == 0 {
|
|
return fmt.Errorf("Missing sentences key in data")
|
|
}
|
|
|
|
return g.db.InitData(adjectives, nouns, verbs, pronouns, data.Sentences)
|
|
}
|
|
|
|
// Prepend "a", "an" or nothing to a phrase
|
|
func (g *english) ana(phrase string) string {
|
|
//fmt.Printf("[ana] phrase[0]: %s; %q\n", string(phrase[0]), phrase)
|
|
if strings.ContainsAny(string(phrase[0]), "aeiou") {
|
|
return "an " + phrase
|
|
}
|
|
|
|
return "a " + phrase
|
|
}
|
|
|
|
// toCap capitalizes the first word of each sentence in the input string.
|
|
func toCap(words string) string {
|
|
ret := strings.ToUpper(string(words[0])) + words[1:]
|
|
|
|
next := strings.Index(words, ". ")
|
|
if next == -1 {
|
|
return ret
|
|
}
|
|
|
|
for next+3 < len(words) {
|
|
newnext := strings.Index(words[next+1:], ". ")
|
|
ret = ret[0:next+2] + strings.ToUpper(string(ret[next+2])) + ret[next+3:]
|
|
|
|
if newnext == -1 {
|
|
break
|
|
}
|
|
next = newnext + next+1
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
func sliceContains(haystack []string, needle string) bool {
|
|
if len(haystack) == 0 {
|
|
return false
|
|
}
|
|
|
|
for _, item := range haystack {
|
|
if item == needle {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|