diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..723ef36 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea \ No newline at end of file diff --git a/README.md b/README.md index 4dc2da7..5b15938 100644 --- a/README.md +++ b/README.md @@ -30,3 +30,104 @@ func main() { return }) ``` +Custom Usage +---- +```go +package your_app + +import ( + "github.com/tjarratt/babble" +) + +func main() { + c := babble.DictionaryConfig { + MinLength: 3, + MaxLength: 5, + } + + babbler := babble.NewBabblerWithConfig(c) + babbler.Separator = "-" + println(babbler.Babble()) // excessive-yak-shaving (or some other phrase) + + // optionally set your own separator + babbler.Separator = " " + println(babbler.Babble()) // "hello from nowhere" (or some other phrase) + + // optionally set the number of words you want + babbler.Count = 1 + println(babbler.Babble()) // antibiomicrobrial (or some other word) + +``` +Upcase +---- +```go +package your_app + +import ( + babble2 "babble" +"github.com/tjarratt/babble" +) + +func main() { + c := babble.DictionaryConfig { + MinLength: 3, + MaxLength: 5, + Upcase: true, + } + + babbler := babble.NewBabblerWithConfig(c) + babbler.Separator = "-" + println(babbler.Babble()) // EXCESSIVE-YAK-SHAVING (or some other phrase) +``` +Downcase +---- +```go + c = babble.DictionaryConfig { + MinLength: 3, + MaxLength: 5, + Downcase: true, + } + + babbler = babble.NewBabblerWithConfig(c) + babbler.Separator = "-" + println(babbler.Babble()) // excessive-yak-shaving (or some other phrase) +``` +Transform +---- +```go + var alternateCase = func (s string)string { + rs, upper := []rune(s), false + for i, r := range rs { + if unicode.IsLetter(r) { + if upper = !upper; upper { + rs[i] = unicode.ToUpper(r) + } + } + } + return string(rs) + } + c = babble.DictionaryConfig { + MinLength: 3, + MaxLength: 5, + TransformWord: alternateCase, + } + + babbler = babble.NewBabblerWithConfig(c) + babbler.Separator = "-" + println(babbler.Babble()) // ExCeSsIvE-YaK-SlUrPiNg the transform is applied to each word individually during load +``` +Custom Word List +---- +```go +customList = []string{ "luke", "leia", "han", "darth", "r2d2" } // C-3PO has been expressly excluded + c = babble.DictionaryConfig { + MinLength: 3, + MaxLength: 5, + CustomWordList: customList, + } + + babbler = babble.NewBabblerWithConfig(c) + babbler.Separator = "-" + println(babbler.Babble()) // luke-darth +``` + diff --git a/babble.go b/babble.go index d65b4c4..d8a0540 100644 --- a/babble.go +++ b/babble.go @@ -6,6 +6,8 @@ import ( "time" ) +var words []string + func init() { rand.Seed(time.Now().UnixNano()) } @@ -14,12 +16,23 @@ type Babbler struct { Count int Separator string Words []string + Dictionary Dictionary } func NewBabbler() (b Babbler) { + d := NewDictionaryWithConfig(DefaultDictionaryConfig) + b.Dictionary = d + b.Count = 2 + b.Separator = "-" + b.Words = d.GetWordList() + return +} +func NewBabblerWithConfig(config DictionaryConfig) (b Babbler) { + d := NewDictionaryWithConfig(config) + b.Dictionary = d b.Count = 2 b.Separator = "-" - b.Words = readAvailableDictionary() + b.Words = b.Dictionary.GetWordList() return } @@ -28,6 +41,5 @@ func (this Babbler) Babble() string { for i := 0; i < this.Count ; i++ { pieces = append(pieces, this.Words[rand.Int()%len(this.Words)]) } - return strings.Join(pieces, this.Separator) } diff --git a/babble_test.go b/babble_test.go index 921dd18..65b853f 100644 --- a/babble_test.go +++ b/babble_test.go @@ -26,4 +26,16 @@ var _ = Describe("babble", func() { Expect(babbler.Babble()).To(Equal("hello☃hello")) }) }) + + Describe("new babble", func(){ + It("handles a basic configuration", func(){ + babbler = NewBabbler() + Expect( len(babbler.Words) ).Should(Equal(babbler.Dictionary.GetListLength())) + }) + It("accepts a custom dictionary config", func(){ + babbler = NewBabblerWithConfig(DictionaryConfig{ MinLength: 2, MaxLength: 4}) + Expect(len(babbler.Words)).Should(Equal(babbler.Dictionary.GetListLength())) + }) + + }) }) diff --git a/dictionary.go b/dictionary.go new file mode 100644 index 0000000..d2e6ab2 --- /dev/null +++ b/dictionary.go @@ -0,0 +1,141 @@ +package babble + +import "strings" + +type ExcludeWord func(s string)bool +type TransformWord func(s string) string + +type DictionaryConfig struct { + // Limits the lower bound to the length of words in the list + MinLength int + // Limits the upper bound to the length of words in the list + MaxLength int + // If this pointer is non-nil, this list is used instead of + // the default OS dictionary. + CustomWordList *[]string + // Changes all letters in each word to lower case + Downcase bool + // Changes all letters in each word to upper case + Upcase bool + // If present, this function is emitted on all words in the dictionary, + // if the provided function returns true, the word is not included in the + // word list. + ExcludeWord ExcludeWord + // Emittted on all words in the dictionary + TransformWord TransformWord +} +var DefaultDictionaryConfig = DictionaryConfig { + MinLength: 3, + MaxLength: 6, +} + +type Dictionary interface { + GetRandomWord() string + GetListLength() int + GetWordList() []string +} + +type babbleDictionary struct { + sourceList []string + config *DictionaryConfig +} +// NewDictionaryWithConfig creates a new dictionary with the supplied +// configuration struct. +// c = babble.DictionaryConfig { +// MinLength: 3, +// MaxLength: 5, +// Downcase: true, +// +// } +// d = NewDictionaryWithConfig(c) +func NewDictionaryWithConfig(c DictionaryConfig) Dictionary { + if c.Upcase && c.Downcase { + panic("cannot upcase and downcase the dictionary at the same time, invalid dictionary config") + } + d := &babbleDictionary{ + config: &c, + sourceList: []string{}, + } + if c.CustomWordList == nil { + d.load() + }else{ + d.sourceList = *c.CustomWordList + if d.config.ExcludeWord != nil { + d.loadWithExclude() + } else { + newSource := []string{} + for _, s := range d.sourceList { + newSource = append(newSource, d.applyTransform(s)) + } + d.sourceList = newSource + } + } + return d +} + +// GetRandomWord returns a random word from the dictionary +func (d *babbleDictionary) GetRandomWord() string { + if d.config.MinLength > d.config.MaxLength { + panic("minimum length cannot exceed maximum length") + } + return getRandomWordFromList(d.config.MinLength, d.config.MaxLength,d.sourceList) +} + +// GetListLength returns the length of the currently loaded word list. +func (d *babbleDictionary) GetListLength() int { + return len(d.sourceList) +} + +// applyTransform applies the configured transformation function +// on each string, and if Upcase or Downcase is true, that +// transformation is applied +func (d *babbleDictionary) applyTransform(s string) (ts string) { + txf := d.config.TransformWord + if txf != nil { + ts = txf(s) + }else{ + ts = s + } + if d.config.Downcase { + return strings.ToLower(ts) + } + if d.config.Upcase { + return strings.ToUpper(ts) + } + return ts +} + +// GetWordList returns the entire list of possible words +func (d *babbleDictionary) GetWordList() []string { + return d.sourceList +} + +// loadWithExclude loads the list of possible words, emitting +// the ExcludeWord function if present to filter candidate +// words. +func (d *babbleDictionary) loadWithExclude() { + exc := d.config.ExcludeWord + newSource := []string{} + for _, s := range d.sourceList{ + if(!exc(s)){ + newSource = append(newSource, d.applyTransform(s)) + } + } + d.sourceList = newSource +} + +// load loads the list of possible words +func (d *babbleDictionary) load() { + list := readAvailableDictionary() + d.sourceList = GenerateEligibleWordList(list, d.config.MinLength, d.config.MaxLength) + if d.config.ExcludeWord != nil { + d.loadWithExclude() + } else { + newSource := []string{} + for _, s := range d.sourceList { + newSource = append(newSource, d.applyTransform(s)) + } + d.sourceList = newSource + } +} + diff --git a/dictionary_test.go b/dictionary_test.go new file mode 100644 index 0000000..ec06e9a --- /dev/null +++ b/dictionary_test.go @@ -0,0 +1,148 @@ +package babble_test + +import ( + "babble" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "strings" +) + +var _ = Describe("dictionary", func() { + var d babble.Dictionary + var w string + BeforeEach(func() { + d = babble.NewDictionaryWithConfig(babble.DictionaryConfig{ MinLength: 3, MaxLength: 5 }) + w = d.GetRandomWord() + }) + + It("returns a random word", func() { + s := d.GetRandomWord() + ls := len(s) + Expect(ls >= 1).Should(BeTrue()) + Expect(ls <= 5).Should(BeTrue()) + + }) + + Describe("with custom configuration", func() { + It("handles min 0 max 30", func() { + d = babble.NewDictionaryWithConfig(babble.DictionaryConfig{MinLength: 0, MaxLength: 30}) + s := d.GetRandomWord() + ls := len(s) + Expect(ls >= 0).Should(BeTrue()) + Expect(ls <= 30).Should(BeTrue()) + }) + + It("handles min 3 max 3", func() { + d = babble.NewDictionaryWithConfig(babble.DictionaryConfig{ MinLength: 3, MaxLength: 3}) + s := d.GetRandomWord() + ls := len(s) + Expect(ls >= 3).Should(BeTrue()) + Expect(ls <= 3).Should(BeTrue()) + }) + + It("handles when min length is larger than max, should use panic", func() { + d = babble.NewDictionaryWithConfig(babble.DictionaryConfig{ MinLength: 30, MaxLength: 3}) + defer func(){ + r := recover(); + Expect(r).ShouldNot(BeNil()) + }() + d.GetRandomWord() + + }) + It("ExcludeWord", func() { + Expect(w).ShouldNot(BeEmpty()) + a := d.GetListLength() + exc := func(s string)bool { + return s == w + } + c := babble.DictionaryConfig{ + MinLength: 3, + MaxLength: 5, + ExcludeWord: exc, + } + d = babble.NewDictionaryWithConfig(c) + b := d.GetListLength() + Expect(b == a - 1).Should(BeTrue()) + }) + It("TransformWord", func() { + Expect(w).ShouldNot(BeEmpty()) + tw := func(s string)string { + r := []rune(s) + for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 { + r[i], r[j] = r[j], r[i] + } + return string(r) + } + c := babble.DictionaryConfig{ + MinLength: 3, + MaxLength: 5, + TransformWord: tw, + } + d = babble.NewDictionaryWithConfig(c) + aw := tw(w) // transform the test word + Expect(aw).ShouldNot(Equal(w)) + words := d.GetWordList() + found := false + for _, txwd := range words { + if txwd == aw { + found = true + } + } + Expect(found).Should(BeTrue()) + + }) + It("uses a custom word list", func(){ + Expect(w).ShouldNot(BeEmpty()) + c := babble.DictionaryConfig{ + MinLength: 3, + MaxLength: 5, + CustomWordList: &[]string{ w }, + } + d = babble.NewDictionaryWithConfig(c) + actual := d.GetRandomWord() + Expect(actual).Should(Equal(w)) + }) + It("upcases", func() { + Expect(w).ShouldNot(BeEmpty()) + c := babble.DictionaryConfig{ + MinLength: 3, + MaxLength: 5, + Upcase: true, + CustomWordList: &[]string{ w }, + } + d = babble.NewDictionaryWithConfig(c) + actual := d.GetRandomWord() + Expect(actual).Should(Equal(strings.ToUpper(w))) + + + }) + It("downcases", func() { + Expect(w).ShouldNot(BeEmpty()) + c := babble.DictionaryConfig{ + MinLength: 3, + MaxLength: 5, + Downcase: true, + CustomWordList: &[]string{ w }, + } + d = babble.NewDictionaryWithConfig(c) + actual := d.GetRandomWord() + Expect(actual).Should(Equal(strings.ToLower(w))) + }) + It("cannot upcase and downcase", func() { + defer func(){ + r := recover(); + Expect(r).ShouldNot(BeNil()) + }() + c := babble.DictionaryConfig{ + MinLength: 3, + MaxLength: 5, + Downcase: true, + Upcase: true, + CustomWordList: &[]string{ w }, + } + d = babble.NewDictionaryWithConfig(c) + + }) + }) +}) + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..54d00b7 --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module babble + +go 1.14 + +require ( + github.com/onsi/ginkgo v1.14.0 + github.com/onsi/gomega v1.10.1 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..677be62 --- /dev/null +++ b/go.sum @@ -0,0 +1,55 @@ +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7 h1:AeiKBIuRw3UomYXSbLy0Mc2dDLfdtbT/IVn4keq83P0= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 h1:DYfZAGf2WMFjMxbgTjaC+2HC7NkNAQs+6Q8b9WEB/F4= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/words.go b/words.go new file mode 100644 index 0000000..bb191c5 --- /dev/null +++ b/words.go @@ -0,0 +1,70 @@ +package babble + +import ( + "fmt" + "math/rand" + "strconv" +) + +type wordIndexByLength map[string][]string + +func addByLength(s string, min int, max int, inMap wordIndexByLength ) { + slen := len(s) + if slen >= min && slen <= max { + l := fmt.Sprintf("%d", slen) + if _, ok := inMap[l]; ok { + inMap[l] = append(inMap[l],s ) + }else{ + inMap[l] = []string{ s } + addByLength(s, min, max, inMap) + } + } + +} + +func sliceWordList(words []string, min int, max int) wordIndexByLength { + if min < 1 || min > max { + min = 1 + } + if max > 36 || min > max { + max = 36 + } + d := map[string][]string {} + + for _, s := range words { + addByLength(s, min, max, d ) + } + return d +} + +func getRandomWordList(min int, max int, list wordIndexByLength) []string { + keys := []int{} + for l := range list { + i, _ := strconv.Atoi(l) + if i > max { + break + } + if i >= min { + keys = append(keys) + } + } + theLength := keys[rand.Int()%len(keys)] + i := string(theLength) + a := list[i] + // return a[rand.Int()%len(a)] + return a +} +func GenerateEligibleWordList(a []string, min int, max int) []string { + words := sliceWordList(a, min, max) + r := []string{} + for _, a := range words { + r = append(r, a...) + } + return r +} +func getRandomWordFromList(min int, max int, wordList []string) string { + if len(wordList) == 0{ + panic("word list is not available") + } + return wordList[rand.Int()%len(wordList)] +} \ No newline at end of file