Skip to content

Commit

Permalink
feat: join strings with a conjunction in a handful of languages
Browse files Browse the repository at this point in the history
  • Loading branch information
meowgorithm committed Oct 25, 2023
1 parent 1cb11ef commit 83c7316
Show file tree
Hide file tree
Showing 5 changed files with 291 additions and 8 deletions.
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,25 @@ into other repositories.

Currently the following packages are available:

* [`higherorder`](./exp/higherorder): generic higher order functions
* [`ordered`](./exp/ordered): generic `min`, `max`, and `clamp` functions for ordered types
* [`slice`](./exp/slice): generic slice utilities
* [`teatest`](./exp/teatest): a library for testing [Bubble Tea](https://github.com/charmbracelet/bubbletea) programs
- [`higherorder`](./exp/higherorder): generic higher order functions
- [`ordered`](./exp/ordered): generic `min`, `max`, and `clamp` functions for ordered types
- [`slice`](./exp/slice): generic slice utilities
- [`strings`](./exp/strings): utilities for working with strings
- [`teatest`](./exp/teatest): a library for testing [Bubble Tea](https://github.com/charmbracelet/bubbletea) programs

## Feedback

We'd love to hear your thoughts on this project. Feel free to drop us a note!

* [Twitter](https://twitter.com/charmcli)
* [The Fediverse](https://mastodon.social/@charmcli)
* [Discord](https://charm.sh/chat)
- [Twitter](https://twitter.com/charmcli)
- [The Fediverse](https://mastodon.social/@charmcli)
- [Discord](https://charm.sh/chat)

## License

[MIT](https://github.com/charmbracelet/x/raw/main/LICENSE)

***
---

Part of [Charm](https://charm.sh).

Expand Down
3 changes: 3 additions & 0 deletions exp/strings/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/charmbracelet/x/exp/strings

go 1.20
132 changes: 132 additions & 0 deletions exp/strings/join.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package strings

// This package works well for some Western languages. PRs for other languages
// are welcome, but do note that implementation for some languages will be less
// straightforward than the ones in use here.

import (
"strings"
)

// Language is a spoken Language.
type Language int

// Available spoken lanaguges.
const (
DE Language = iota
DK
EN
ES
FR
IT
NO
PT
SE
)

// String returns the English name of the [Language] code.
func (l Language) String() string {
return map[Language]string{
DE: "German",
DK: "Danish",
EN: "English",
ES: "Spanish",
FR: "French",
IT: "Italian",
NO: "Norwegian",
PT: "Portuguese",
SE: "Swedish",
}[l]
}

func (l Language) conjuction() string {
switch l {
case DE:
return "und"
case DK:
return "og"
case EN:
return "and"
case ES:
return "y"
case FR:
return "et"
case NO:
return "og"
case IT:
return "e"
case PT:
return "e"
case SE:
return "och"
default:
return ""
}
}

func (l Language) separator() string {
switch l {
case DE, DK, EN, ES, FR, NO, IT, PT, SE:
return ", "
default:
return " "
}
}

// EnglishJoin joins a slice of strings with commas and the "and" conjugation
// before the final item. The Oxford comma can optionally be applied.
//
// Example:
//
// str := EnglishJoin([]string{"meow", "purr", raow"}, EN, true)
// fmt.Println(str) // meow, purr, and raow
func EnglishJoin(words []string, oxfordComma bool) string {
return spokenLangJoin(words, EN, oxfordComma)
}

// SpokenLangaugeJoin joins a slice of strings with commas and a conjuction
// before the final item. You may specify the language with [Language].
//
// If you are using English and need the Oxford Comma, use [EnglishJoin].
//
// Example:
//
// str := SpokenLanguageJoin([]string{"eins", "zwei", drei"}, DE)
// fmt.Println(str) // eins, zwei und drei
func SpokenLanguageJoin(words []string, language Language) string {
return spokenLangJoin(words, language, false)
}

func spokenLangJoin(words []string, language Language, oxfordComma bool) string {
conjuction := language.conjuction() + " "
separator := language.separator()

b := strings.Builder{}
for i, word := range words {
if word == "" {
continue
}

if i == 0 {
b.WriteString(word)
continue
}

// Is this the final word?
if len(words) > 1 && i == len(words)-1 {
// Apply the Oxford comma if requested as long as the language is
// English.
if language == EN && oxfordComma && i > 1 {
b.WriteString(separator)
} else {
b.WriteRune(' ')
}

b.WriteString(conjuction + word)
continue
}

b.WriteString(separator + word)
}
return b.String()
}
146 changes: 146 additions & 0 deletions exp/strings/join_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package strings

import "testing"

func TestEnglishJoin(t *testing.T) {
for i, tc := range []struct {
words []string
lang Language
oxfordComma bool
expected string
}{
{
words: []string{"one", "two", "three"},
lang: EN,
oxfordComma: true,
expected: "one, two, and three",
},
{
words: []string{"one", "two", "three", "four"},
oxfordComma: true,
expected: "one, two, three, and four",
},
{
words: []string{"one", "two"},
oxfordComma: true,
expected: "one and two",
},
{
words: []string{"one", "two"},
oxfordComma: true,
expected: "one and two",
},
{
words: []string{"one", "two", "three"},
oxfordComma: false,
expected: "one, two and three",
},
{
words: []string{"one"},
oxfordComma: true,
expected: "one",
},
} {
actual := EnglishJoin(tc.words, tc.oxfordComma)
if actual != tc.expected {
t.Errorf("Test #%d:\n expected: %q\n got: %q", i+1, tc.expected, actual)
}
}
}

func TestSpokenLanguageJoin(t *testing.T) {
for i, tc := range []struct {
words []string
lang Language
expected string
}{
// Test for correct commas and conjunctions in each language.
{
words: []string{"eins", "zwei", "drei"},
lang: DE,
expected: "eins, zwei und drei",
},
{
words: []string{"en", "to", "tre"},
lang: DK,
expected: "en, to og tre",
},
{
words: []string{"one", "two", "three"},
lang: EN,
expected: "one, two and three",
},
{
words: []string{"uno", "dos", "tres"},
lang: ES,
expected: "uno, dos y tres",
},
{
words: []string{"un", "deux", "trois"},
lang: FR,
expected: "un, deux et trois",
},
{
words: []string{"uno", "due", "tre"},
lang: IT,
expected: "uno, due e tre",
},
{
words: []string{"en", "to", "tre"},
lang: NO,
expected: "en, to og tre",
},
{
words: []string{"um", "dois", "três"},
lang: PT,
expected: "um, dois e três",
},
{
words: []string{"ett", "två", "tre", "fyra"},
lang: SE,
expected: "ett, två, tre och fyra",
},

// Test other things.
{
words: []string{"one", "two", "three", "four"},
lang: EN,
expected: "one, two, three and four",
},
{
words: []string{"um", "dois"},
lang: PT,
expected: "um e dois",
},
{
words: []string{"un", "deux"},
lang: FR,
expected: "un et deux",
},
{
words: []string{"one"},
lang: EN,
expected: "one",
},
{
words: []string{},
lang: EN,
expected: "",
},
{
words: []string{"", "", ""},
lang: EN,
expected: "",
},
{
words: nil,
lang: EN,
expected: "",
},
} {
actual := SpokenLanguageJoin(tc.words, tc.lang)
if actual != tc.expected {
t.Errorf("Test #%d:\n expected: %q\n got: %q", i+1, tc.expected, actual)
}
}
}
1 change: 1 addition & 0 deletions go.work
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ use (
./exp/higherorder
./exp/ordered
./exp/slice
./exp/strings
./exp/teatest
)

0 comments on commit 83c7316

Please sign in to comment.