├── .gitignore ├── go.mod ├── strings ├── humanize_test.go ├── truncate.go ├── bool_test.go ├── humanize.go ├── camelize_test.go ├── bool.go ├── truncate_test.go └── camelize.go ├── collection ├── oxford_test.go └── oxford.go ├── .github └── workflows │ ├── release.yml │ ├── test.yml │ └── codecov.yml ├── numbers ├── ordinal.go ├── ordinal_test.go ├── roman_test.go └── roman.go ├── units ├── binary_suffix_test.go └── binary_suffix.go ├── LICENSE ├── time ├── difference.go └── difference_test.go ├── go.sum └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /profile.out 2 | /coverage.txt 3 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sbani/go-humanizer 2 | 3 | go 1.19 4 | 5 | require github.com/stretchr/testify v1.8.1 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.1 // indirect 9 | github.com/pmezard/go-difflib v1.0.0 // indirect 10 | gopkg.in/yaml.v3 v3.0.1 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /strings/humanize_test.go: -------------------------------------------------------------------------------- 1 | package strings 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestHumanize(t *testing.T) { 10 | assert.Equal(t, "News count", Humanize("news_count", true)) 11 | assert.Equal(t, "user", Humanize("User", false)) 12 | assert.Equal(t, "News", Humanize("news_id", true)) 13 | } 14 | -------------------------------------------------------------------------------- /collection/oxford_test.go: -------------------------------------------------------------------------------- 1 | package collection 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestOxford(t *testing.T) { 10 | assert.Equal(t, "", Oxford([]string{}, -1)) 11 | 12 | var testCollection = []string{ 13 | "Albert", 14 | "Norbert", 15 | "Michael", 16 | "Kevin", 17 | } 18 | 19 | assert.Equal(t, "Albert", Oxford(testCollection[:1], -1)) 20 | assert.Equal(t, "Albert and Norbert", Oxford(testCollection[:2], -1)) 21 | assert.Equal(t, "Albert, Norbert, Michael, and Kevin", Oxford(testCollection, -1)) 22 | assert.Equal(t, "Albert, Norbert, and 2 more", Oxford(testCollection, 2)) 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | build: 10 | 11 | name: Create Release 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v2 18 | 19 | - name: Create Release 20 | id: create_release 21 | uses: actions/create-release@v1 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.TOKEN }} 24 | with: 25 | tag_name: ${{ github.ref }} 26 | release_name: Release ${{ github.ref }} 27 | draft: false 28 | prerelease: false 29 | -------------------------------------------------------------------------------- /numbers/ordinal.go: -------------------------------------------------------------------------------- 1 | package numbers 2 | 3 | import ( 4 | "math" 5 | "strconv" 6 | ) 7 | 8 | // Ordinal returns the ordinal string for a specific integer. 9 | func Ordinal(number int) string { 10 | absNumber := int(math.Abs(float64(number))) 11 | 12 | i := absNumber % 100 13 | if i == 11 || i == 12 || i == 13 { 14 | return "th" 15 | } 16 | 17 | switch absNumber % 10 { 18 | case 1: 19 | return "st" 20 | case 2: 21 | return "nd" 22 | case 3: 23 | return "rd" 24 | default: 25 | return "th" 26 | } 27 | } 28 | 29 | // Ordinalize the number by adding the Ordinal to the number. 30 | func Ordinalize(number int) string { 31 | return strconv.Itoa(number) + Ordinal(number) 32 | } 33 | -------------------------------------------------------------------------------- /strings/truncate.go: -------------------------------------------------------------------------------- 1 | package strings 2 | 3 | import "strings" 4 | 5 | // Truncate a string, but never cut a word. So if you would cut the word it will 6 | // be return completely (to the end or the next whitespace) 7 | func Truncate(s string, charactersCount int) string { 8 | strLen := len(s) 9 | if charactersCount < 0 || strLen <= charactersCount { 10 | return s 11 | } 12 | 13 | length := strLen 14 | breakpoint := strings.Index(s[charactersCount:], " ") 15 | if breakpoint != -1 { 16 | length = breakpoint + charactersCount 17 | } else if s[(charactersCount-1):charactersCount] == " " { 18 | length = charactersCount 19 | } 20 | 21 | return strings.TrimRight(s[:length], " ") 22 | } 23 | -------------------------------------------------------------------------------- /strings/bool_test.go: -------------------------------------------------------------------------------- 1 | package strings 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestToBool(t *testing.T) { 10 | tests := map[string]bool{ 11 | "yes": true, 12 | "no": false, 13 | "y": true, 14 | "n": false, 15 | "true": true, 16 | "false": false, 17 | "on": true, 18 | "off": false, 19 | "1": true, 20 | "0": false, 21 | } 22 | 23 | for input, expected := range tests { 24 | out, err := ToBool(input) 25 | assert.Equal(t, expected, out) 26 | assert.Nil(t, err) 27 | } 28 | } 29 | 30 | func TestToBoolFail(t *testing.T) { 31 | out, err := ToBool("ILikeApple") 32 | assert.NotNil(t, err) 33 | assert.False(t, out) 34 | } 35 | -------------------------------------------------------------------------------- /numbers/ordinal_test.go: -------------------------------------------------------------------------------- 1 | package numbers 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestOrdinalizesNumbers(t *testing.T) { 10 | assert.Equal(t, "1st", Ordinalize(1)) 11 | assert.Equal(t, "2nd", Ordinalize(2)) 12 | assert.Equal(t, "23rd", Ordinalize(23)) 13 | assert.Equal(t, "1002nd", Ordinalize(1002)) 14 | assert.Equal(t, "-111th", Ordinalize(-111)) 15 | assert.Equal(t, "5th", Ordinalize(5)) 16 | } 17 | 18 | func TestReturnsOrdinalSuffix(t *testing.T) { 19 | assert.Equal(t, "st", Ordinal(1)) 20 | assert.Equal(t, "nd", Ordinal(2)) 21 | assert.Equal(t, "rd", Ordinal(23)) 22 | assert.Equal(t, "nd", Ordinal(1002)) 23 | assert.Equal(t, "th", Ordinal(-111)) 24 | assert.Equal(t, "th", Ordinal(5)) 25 | } 26 | -------------------------------------------------------------------------------- /units/binary_suffix_test.go: -------------------------------------------------------------------------------- 1 | package units 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestBinarySuffix(t *testing.T) { 10 | tests := map[float64]string{ 11 | 0: "0 bytes", 12 | 1: "1 bytes", 13 | 1024: "1.0 kB", 14 | 1025: "1.0 kB", 15 | 1536: "1.5 kB", 16 | 1048576 * 5: "5.00 MB", 17 | 1073741824 * 2: "2.00 GB", 18 | 1099511627776 * 3: "3.00 TB", 19 | 1325899906842623: "1.18 PB", 20 | -5: "-5 bytes", 21 | -1.49: "-1 bytes", 22 | -2.8: "-3 bytes", 23 | } 24 | 25 | for input, expected := range tests { 26 | assert.Equal(t, expected, BinarySuffix(input)) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run Code Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | 8 | strategy: 9 | matrix: 10 | go-version: [1.20.x, 1.19.x, 1.18.x, 1.17.x, 1.16.x, 1.15.x, 1.14.x, 1.13.x] 11 | os: [ubuntu-latest, macos-latest, windows-latest] 12 | 13 | runs-on: ${{ matrix.os }} 14 | 15 | steps: 16 | - name: Install Go 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: ${{ matrix.go-version }} 20 | 21 | - name: Checkout code 22 | uses: actions/checkout@v2 23 | 24 | - name: Build 25 | run: go build -v ./... 26 | 27 | - name: Test 28 | run: | 29 | go get -u github.com/stretchr/testify 30 | go test ./... 31 | -------------------------------------------------------------------------------- /strings/humanize.go: -------------------------------------------------------------------------------- 1 | package strings 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | ) 7 | 8 | // ForbiddenWords are words that will be removed from text (in Humanize func) 9 | var ForbiddenWords = []string{ 10 | "id", 11 | } 12 | 13 | // Humanize a string means to remove underscores and return string as lower. 14 | // String can be capitalized 15 | func Humanize(s string, capitalize bool) string { 16 | s = regexp.MustCompile("([A-Z])").ReplaceAllString(s, "_$1") 17 | s = regexp.MustCompile("[_\\s]+").ReplaceAllString(s, " ") 18 | s = strings.ToLower(s) 19 | 20 | for _, word := range ForbiddenWords { 21 | s = strings.Replace(s, word, "", -1) 22 | } 23 | 24 | s = strings.TrimSpace(s) 25 | 26 | if capitalize { 27 | s = strings.ToUpper(s[:1]) + s[1:] 28 | } 29 | 30 | return s 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | name: Running Code Coverage 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | go-version: [1.17.x] 13 | 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v2 17 | with: 18 | fetch-depth: 2 19 | 20 | - name: Install Go 21 | uses: actions/setup-go@v2 22 | with: 23 | go-version: ${{ matrix.go-version }} 24 | 25 | - name: Test With Coverage 26 | run: go test -v -coverprofile=coverage.txt -covermode=atomic ./... 27 | 28 | - name: Upload coverage to Codecov 29 | uses: codecov/codecov-action@v1 30 | with: 31 | token: ${{ secrets.CODECOV_TOKEN }} 32 | -------------------------------------------------------------------------------- /strings/camelize_test.go: -------------------------------------------------------------------------------- 1 | package strings 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestCamelize(t *testing.T) { 10 | tests := map[string]string{ 11 | "foo_bar": "fooBar", 12 | "Foo_Bar": "fooBar", 13 | "FOO_BAR": "fooBar", 14 | "foo__bar": "fooBar", 15 | "b": "b", 16 | "b_": "b", 17 | "one_two_three_four": "oneTwoThreeFour", 18 | } 19 | 20 | for input, expected := range tests { 21 | assert.Equal(t, expected, Camelize(input)) 22 | } 23 | } 24 | 25 | func TestUnderscore(t *testing.T) { 26 | tests := map[string]string{ 27 | "oneTwo": "one_two", 28 | "oneTwoThreeFour": "one_two_three_four", 29 | } 30 | 31 | for input, expected := range tests { 32 | assert.Equal(t, expected, Underscore(input)) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /strings/bool.go: -------------------------------------------------------------------------------- 1 | package strings 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // TrueValues is a list of words which leads to true 8 | var TrueValues = []string{ 9 | "yes", 10 | "y", 11 | "true", 12 | "on", 13 | "1", 14 | } 15 | 16 | // FalseValues is a list of words which leads to false 17 | var FalseValues = []string{ 18 | "no", 19 | "n", 20 | "false", 21 | "off", 22 | "0", 23 | } 24 | 25 | // ToBool converts a string value to boolean 26 | func ToBool(input string) (bool, error) { 27 | if isInSlice(input, TrueValues) { 28 | return true, nil 29 | } else if isInSlice(input, FalseValues) { 30 | return false, nil 31 | } 32 | 33 | return false, fmt.Errorf("Can't convert value \"%s\" to boolean", input) 34 | } 35 | 36 | // Checks if value is in slice 37 | func isInSlice(value string, slice []string) bool { 38 | for _, v := range slice { 39 | if v == value { 40 | return true 41 | } 42 | } 43 | return false 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Sufijen Bani 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /strings/truncate_test.go: -------------------------------------------------------------------------------- 1 | package strings 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestTruncate(t *testing.T) { 10 | text := "Lorem ipsum dolorem si amet, lorem ipsum. Dolorem sic et nunc." 11 | assert.Equal(t, "Lorem", Truncate(text, 2)) 12 | assert.Equal(t, "Lorem ipsum", Truncate(text, 10)) 13 | assert.Equal(t, "Lorem ipsum dolorem si amet, lorem", Truncate(text, 30)) 14 | assert.Equal(t, "Lorem", Truncate(text, 0)) 15 | assert.Equal(t, text, Truncate(text, -2)) 16 | 17 | textShort := "Short text" 18 | assert.Equal(t, "Short", Truncate(textShort, 1)) 19 | assert.Equal(t, "Short", Truncate(textShort, 2)) 20 | assert.Equal(t, "Short", Truncate(textShort, 3)) 21 | assert.Equal(t, "Short", Truncate(textShort, 4)) 22 | assert.Equal(t, "Short", Truncate(textShort, 5)) 23 | assert.Equal(t, "Short", Truncate(textShort, 6)) 24 | assert.Equal(t, "Short text", Truncate(textShort, 7)) 25 | assert.Equal(t, "Short text", Truncate(textShort, 8)) 26 | assert.Equal(t, "Short text", Truncate(textShort, 9)) 27 | assert.Equal(t, "Short text", Truncate(textShort, 10)) 28 | assert.Equal(t, "Short text", Truncate(textShort, 100)) 29 | } 30 | -------------------------------------------------------------------------------- /units/binary_suffix.go: -------------------------------------------------------------------------------- 1 | package units 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "sort" 7 | ) 8 | 9 | // BinaryConvertThreshold is the number for a binary "step" 10 | const BinaryConvertThreshold = 1024 11 | 12 | // BinarySuffix returns the given number with a binary suffix. 13 | func BinarySuffix(number float64) string { 14 | var str string 15 | if number < 0 { 16 | str = "-" 17 | number = math.Abs(number) 18 | } 19 | var binarySuffixPrefixes = map[float64]string{ 20 | 1125899906842624: "%.2f PB", 21 | 1099511627776: "%.2f TB", 22 | 1073741824: "%.2f GB", 23 | 1048576: "%.2f MB", 24 | 1024: "%.1f kB", 25 | } 26 | intSlice := make([]float64, 0, len(binarySuffixPrefixes)) 27 | for num := range binarySuffixPrefixes { 28 | intSlice = append(intSlice, num) 29 | } 30 | sort.Sort(sort.Reverse(sort.Float64Slice(intSlice))) 31 | for _, size := range intSlice { 32 | if size <= number { 33 | var value float64 34 | if number >= BinaryConvertThreshold { 35 | value = number / float64(size) 36 | } 37 | return str + fmt.Sprintf(binarySuffixPrefixes[size], value) 38 | } 39 | } 40 | return fmt.Sprintf("%s%.0f bytes", str, number) 41 | } 42 | -------------------------------------------------------------------------------- /strings/camelize.go: -------------------------------------------------------------------------------- 1 | package strings 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | ) 7 | 8 | // Camelize the string 9 | func Camelize(s string) string { 10 | words := strings.Split(s, "_") 11 | for i, word := range words { 12 | words[i] = ucFirst(strings.ToLower(word)) 13 | } 14 | 15 | return lcFirst(strings.Join(words, "")) 16 | } 17 | 18 | // Underscore the string 19 | func Underscore(s string) string { 20 | reg := regexp.MustCompile("[A-Z]+") 21 | indexes := reg.FindAllStringIndex(s, -1) 22 | laststart := 0 23 | words := make([]string, len(indexes)+1) 24 | for i, element := range indexes { 25 | words[i] = s[laststart:element[0]] 26 | element[1]-- 27 | laststart = element[1] 28 | } 29 | words[len(indexes)] = s[laststart:] 30 | 31 | return strings.ToLower(strings.Join(words, "_")) 32 | } 33 | 34 | // Lower case first letter of given string 35 | func lcFirst(s string) string { 36 | if len(s) <= 1 { 37 | return strings.ToLower(s) 38 | } 39 | return strings.ToLower(s[:1]) + s[1:] 40 | } 41 | 42 | // Upper case first letter of given string 43 | func ucFirst(s string) string { 44 | if len(s) <= 1 { 45 | return strings.ToUpper(s) 46 | } 47 | return strings.ToUpper(s[:1]) + s[1:] 48 | } 49 | -------------------------------------------------------------------------------- /numbers/roman_test.go: -------------------------------------------------------------------------------- 1 | package numbers 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | var workingTests = map[string]int{ 10 | "I": 1, 11 | "XC": 90, 12 | "DCCCXCIX": 899, 13 | "IX": 9, 14 | "CXXV": 125, 15 | "MMMMCMXCIX": 4999, 16 | } 17 | 18 | func TestToRoman(t *testing.T) { 19 | for expected, input := range workingTests { 20 | output, err := ToRoman(input) 21 | 22 | if err == nil { 23 | assert.Equal(t, expected, output) 24 | } else { 25 | assert.Fail(t, "An error was given unexpectatly") 26 | } 27 | } 28 | } 29 | 30 | func TestToRomanError(t *testing.T) { 31 | tests := []int{ 32 | -1, 33 | 0, 34 | } 35 | 36 | for _, input := range tests { 37 | _, err := ToRoman(input) 38 | assert.EqualError(t, err, "Number not convertable") 39 | } 40 | } 41 | 42 | func TestFromRoman(t *testing.T) { 43 | for input, expected := range workingTests { 44 | output, err := FromRoman(input) 45 | 46 | if err == nil { 47 | assert.Equal(t, expected, output) 48 | } else { 49 | assert.Fail(t, "An error was given unexpectatly") 50 | } 51 | } 52 | } 53 | 54 | func TestFromRomanError(t *testing.T) { 55 | tests := []string{ 56 | "foobar", 57 | "", 58 | } 59 | 60 | for _, input := range tests { 61 | _, err := FromRoman(input) 62 | assert.EqualError(t, err, "String not convertable") 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /time/difference.go: -------------------------------------------------------------------------------- 1 | package time 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "time" 7 | ) 8 | 9 | // Difference finds the difference between t1 and t2. 10 | func Difference(t1, t2 time.Time) string { 11 | seconds := math.Abs(t1.Sub(t2).Seconds()) 12 | if seconds < 1 { 13 | return "just now" 14 | } 15 | var end string 16 | if t1.After(t2) { 17 | end = "ago" 18 | } else { 19 | end = "from now" 20 | } 21 | // Minute 22 | minutes := seconds / 60 23 | if minutes < 1 { 24 | return createString(seconds, "second", end) 25 | } 26 | // Hour 27 | hours := minutes / 60 28 | if hours < 1 { 29 | return createString(minutes, "minute", end) 30 | } 31 | // Days 32 | days := hours / 24 33 | if days < 1 { 34 | return createString(hours, "hour", end) 35 | } 36 | // Weeks 37 | weeks := days / 7 38 | if weeks < 1 { 39 | return createString(days, "day", end) 40 | } 41 | // Months 42 | months := weeks / 4 43 | if months < 1 { 44 | return createString(weeks, "week", end) 45 | } 46 | // Years 47 | years := months / 12 48 | if years < 1 { 49 | return createString(months, "month", end) 50 | } 51 | return createString(years, "year", end) 52 | } 53 | 54 | func createString(n float64, precision string, end string) string { 55 | n = math.Floor(n + 0.5) 56 | if n > 1 { 57 | return fmt.Sprintf("%d %ss %s", int(n), precision, end) 58 | } 59 | return fmt.Sprintf("%d %s %s", int(n), precision, end) 60 | } 61 | -------------------------------------------------------------------------------- /collection/oxford.go: -------------------------------------------------------------------------------- 1 | package collection 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | ) 7 | 8 | // Oxford returns a string from a string collection by adding ", " as a separator and the word "and" for the last separator. 9 | // A limit will print all elements to the limit and elements after the limit will be grouped with a "and n more". 10 | // No limit is represented with with a limit <= 0 11 | func Oxford(collection []string, limit int) string { 12 | len := len(collection) 13 | 14 | switch len { 15 | case 0: 16 | return "" 17 | case 1: 18 | return collection[0] 19 | case 2: 20 | return formatOnlyTwo(collection) 21 | } 22 | 23 | if limit > 0 { 24 | return formatCommaSeparatedWithLimit(collection, limit, len) 25 | } 26 | 27 | return formatCommaSeparated(collection, len) 28 | } 29 | 30 | // Format a collection of two strings 31 | func formatOnlyTwo(collection []string) string { 32 | return collection[0] + " and " + collection[1] 33 | } 34 | 35 | // Format with limit 36 | func formatCommaSeparatedWithLimit(collection []string, limit int, count int) string { 37 | display := strings.Join(collection[:limit], ", ") 38 | moreCount := count - limit 39 | return display + ", and " + strconv.Itoa(moreCount) + " more" 40 | } 41 | 42 | // Format without limit 43 | func formatCommaSeparated(collection []string, count int) string { 44 | display := strings.Join(collection[:(count-1)], ", ") 45 | return display + ", and " + collection[(count-1)] 46 | } 47 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 5 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 6 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 7 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 8 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 9 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 10 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 11 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 12 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 13 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 14 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 15 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 16 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 17 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 18 | -------------------------------------------------------------------------------- /numbers/roman.go: -------------------------------------------------------------------------------- 1 | package numbers 2 | 3 | import ( 4 | "errors" 5 | "regexp" 6 | "sort" 7 | ) 8 | 9 | // MinValue is the min number which should be converted 10 | const MinValue = 1 11 | 12 | // RomanStringMatcher is a regex to check if string is a roman number 13 | const RomanStringMatcher = "^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$" 14 | 15 | // ToRoman converts a number to a roman string. Number has to be higher than MinValue 16 | func ToRoman(number int) (string, error) { 17 | if number < MinValue { 18 | return "", errors.New("Number not convertable") 19 | } 20 | 21 | table := map[int]string{ 22 | 1000: "M", 23 | 900: "CM", 24 | 500: "D", 25 | 400: "CD", 26 | 100: "C", 27 | 90: "XC", 28 | 50: "L", 29 | 40: "XL", 30 | 10: "X", 31 | 9: "IX", 32 | 5: "V", 33 | 4: "IV", 34 | 1: "I", 35 | } 36 | 37 | var sortedArabs []int 38 | for arab := range table { 39 | sortedArabs = append(sortedArabs, arab) 40 | } 41 | sort.Sort(sort.Reverse(sort.IntSlice(sortedArabs))) 42 | 43 | romanString := "" 44 | for number > 0 { 45 | for _, arab := range sortedArabs { 46 | if number >= arab { 47 | number -= arab 48 | romanString += table[arab] 49 | break 50 | } 51 | } 52 | } 53 | 54 | return romanString, nil 55 | } 56 | 57 | // FromRoman converts a roman string to an integer. Returns an error if string is not a roman number. 58 | func FromRoman(input string) (int, error) { 59 | i := len(input) 60 | 61 | if i == 0 || !regexp.MustCompile(RomanStringMatcher).MatchString(input) { 62 | return 0, errors.New("String not convertable") 63 | } 64 | 65 | var table = map[string]int{ 66 | "M": 1000, 67 | "CM": 900, 68 | "D": 500, 69 | "CD": 400, 70 | "C": 100, 71 | "XC": 90, 72 | "L": 50, 73 | "XL": 40, 74 | "X": 10, 75 | "IX": 9, 76 | "V": 5, 77 | "IV": 4, 78 | "I": 1, 79 | } 80 | 81 | total := 0 82 | for i > 0 { 83 | i-- 84 | digit := table[string(input[i])] 85 | if i > 0 { 86 | previousDigit := table[string(input[(i-1)])] 87 | if previousDigit < digit { 88 | digit -= previousDigit 89 | i-- 90 | } 91 | } 92 | total += digit 93 | } 94 | return total, nil 95 | } 96 | -------------------------------------------------------------------------------- /time/difference_test.go: -------------------------------------------------------------------------------- 1 | package time 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestDifference(t *testing.T) { 11 | baseTime := time.Date(2016, 11, 3, 13, 0, 0, 0, time.UTC) 12 | assert.Equal(t, 13 | "just now", 14 | Difference( 15 | baseTime, 16 | baseTime, 17 | ), 18 | ) 19 | assert.Equal(t, 20 | "just now", 21 | Difference( 22 | baseTime, 23 | baseTime.Add(2*time.Nanosecond), 24 | ), 25 | ) 26 | assert.Equal(t, 27 | "5 seconds from now", 28 | Difference( 29 | baseTime, 30 | baseTime.Add(5*time.Second), 31 | ), 32 | ) 33 | assert.Equal(t, 34 | "1 minute ago", 35 | Difference( 36 | baseTime, 37 | baseTime.Add(-61*time.Second), 38 | ), 39 | ) 40 | assert.Equal(t, 41 | "15 minutes ago", 42 | Difference( 43 | baseTime, 44 | baseTime.Add(-(15*time.Minute+3*time.Nanosecond)), 45 | ), 46 | ) 47 | assert.Equal(t, 48 | "15 minutes from now", 49 | Difference( 50 | baseTime, 51 | baseTime.Add(14*time.Minute+50*time.Second), 52 | ), 53 | ) 54 | assert.Equal(t, 55 | "59 minutes ago", 56 | Difference( 57 | baseTime, 58 | baseTime.Add(-(59*time.Minute)), 59 | ), 60 | ) 61 | assert.Equal(t, 62 | "1 hour from now", 63 | Difference( 64 | baseTime, 65 | baseTime.Add(1*time.Hour+2*time.Minute), 66 | ), 67 | ) 68 | assert.Equal(t, 69 | "2 hours from now", 70 | Difference( 71 | baseTime, 72 | baseTime.Add(2*time.Hour+3*time.Minute), 73 | ), 74 | ) 75 | assert.Equal(t, 76 | "1 hour ago", 77 | Difference( 78 | baseTime, 79 | baseTime.Add(-(61*time.Minute)), 80 | ), 81 | ) 82 | assert.Equal(t, 83 | "1 day ago", 84 | Difference( 85 | baseTime, 86 | baseTime.Add(-(25*time.Hour)), 87 | ), 88 | ) 89 | assert.Equal(t, 90 | "2 days ago", 91 | Difference( 92 | baseTime, 93 | baseTime.Add(-(2*24*time.Hour+2*time.Hour)), 94 | ), 95 | ) 96 | assert.Equal(t, 97 | "1 day from now", 98 | Difference( 99 | baseTime, 100 | baseTime.Add(24*time.Hour), 101 | ), 102 | ) 103 | assert.Equal(t, 104 | "2 days from now", 105 | Difference( 106 | baseTime, 107 | baseTime.Add(47*time.Hour), 108 | ), 109 | ) 110 | assert.Equal(t, 111 | "1 week from now", 112 | Difference( 113 | baseTime, 114 | baseTime.Add(8*24*time.Hour), 115 | ), 116 | ) 117 | assert.Equal(t, 118 | "2 weeks from now", 119 | Difference( 120 | baseTime, 121 | baseTime.Add(14*24*time.Hour), 122 | ), 123 | ) 124 | assert.Equal(t, 125 | "1 month ago", 126 | Difference( 127 | baseTime, 128 | baseTime.Add(-31*24*time.Hour), 129 | ), 130 | ) 131 | assert.Equal(t, 132 | "5 months from now", 133 | Difference( 134 | baseTime, 135 | baseTime.Add(5*30*24*time.Hour-2*time.Hour), 136 | ), 137 | ) 138 | assert.Equal(t, 139 | "1 year from now", 140 | Difference( 141 | baseTime, 142 | baseTime.Add(366*24*time.Hour), 143 | ), 144 | ) 145 | assert.Equal(t, 146 | "2 years ago", 147 | Difference( 148 | baseTime, 149 | baseTime.Add(-(2*367*24*time.Hour+5*time.Hour)), 150 | ), 151 | ) 152 | } 153 | 154 | func TestCreateString(t *testing.T) { 155 | assert.Equal(t, "1 foo bar", createString(float64(1), "foo", "bar")) 156 | assert.Equal(t, "1 foo bar", createString(float64(1.002), "foo", "bar")) 157 | assert.Equal(t, "2 foos bar", createString(float64(2), "foo", "bar")) 158 | assert.Equal(t, "3 foos bar", createString(float64(2.9), "foo", "bar")) 159 | assert.Equal(t, "102 foos bar", createString(float64(101.5), "foo", "bar")) 160 | assert.Equal(t, "101 foos bar", createString(float64(101.4), "foo", "bar")) 161 | } 162 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Humanizer 2 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE.md) ![Test Workflow](https://github.com/sbani/go-humanizer/actions/workflows/test.yml/badge.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/sbani/go-humanizer)](https://goreportcard.com/report/github.com/sbani/go-humanizer) [![codecov](https://codecov.io/gh/sbani/go-humanizer/branch/master/graph/badge.svg)](https://codecov.io/gh/sbani/go-humanizer) [![GoDoc](https://godoc.org/github.com/sbani/go-humanizer?status.svg)](https://godoc.org/github.com/sbani/go-humanizer) 3 | 4 | Humanize values to make them easier to read. 5 | 6 | ## Installation 7 | ```bash 8 | go get github.com/sbani/go-humanizer 9 | ``` 10 | 11 | ## Usage 12 | 13 | ### Strings 14 | 15 | #### Humanize 16 | ```go 17 | import "github.com/sbani/go-humanizer/strings" 18 | 19 | Humanize("news_count", true) // "News count" 20 | Humanize("User", false) // "user" 21 | Humanize("news_id", true) // "News" 22 | ``` 23 | 24 | #### Truncate 25 | Truncate string but never cut within a word. 26 | ```go 27 | import "github.com/sbani/go-humanizer/strings" 28 | 29 | textShort := "Short text" 30 | Truncate(textShort, 1) // Short 31 | Truncate(textShort, 6) // Short 32 | Truncate(textShort, 7) // Short text 33 | ``` 34 | 35 | #### Bool 36 | ```go 37 | import "github.com/sbani/go-humanizer/strings" 38 | 39 | ToBool(textShort, "no") // false 40 | ToBool(textShort, "false") // false 41 | ToBool(textShort, "yes") // true 42 | ``` 43 | ### Time 44 | 45 | #### Difference 46 | ```go 47 | import "github.com/sbani/go-humanizer/time" 48 | 49 | baseTime := time.Date(2016, 11, 3, 13, 0, 0, 0, time.UTC) 50 | Difference(baseTime, baseTime) // "just now" 51 | Difference(baseTime, baseTime.Add(5*time.Second)) // "5 seconds from now" 52 | Difference(baseTime, baseTime.Add(-61*time.Second)) // "1 minute ago" 53 | Difference(baseTime, baseTime.Add(-(15*time.Minute+3*time.Nanosecond))) // "15 minutes ago" 54 | Difference(baseTime, baseTime.Add(2*time.Hour+3*time.Minute)) // "2 hours from now" 55 | Difference(baseTime, baseTime.Add(-(25*time.Hour))) // "1 day ago" 56 | Difference(baseTime, baseTime.Add(14*24*time.Hour)) // "2 weeks from now" 57 | Difference(baseTime, baseTime.Add(-31*24*time.Hour)) // "1 month ago" 58 | Difference(baseTime, baseTime.Add(366*24*time.Hour)) // "1 year from now" 59 | ``` 60 | 61 | ### Units 62 | 63 | #### Binary Suffix 64 | ```go 65 | import "github.com/sbani/go-humanizer/units" 66 | 67 | s := BinarySuffix(0) // "0 bytes" 68 | s := BinarySuffix(1536) // "1.5 kB" 69 | s := BinarySuffix(1048576 * 5) // "5.00 MB" 70 | s := BinarySuffix(1073741824 * 2) // "2.00 GB" 71 | ``` 72 | ### Numbers 73 | 74 | #### Ordinalize 75 | ```go 76 | import "github.com/sbani/go-humanizer/numbers" 77 | 78 | Ordinalize(0) // "0th" 79 | Ordinalize(1) // "1st" 80 | Ordinalize(2) // "2nd" 81 | Ordinalize(23) // "23rd" 82 | Ordinalize(1002) // "1002nd" 83 | Ordinalize(-111) // "-111th" 84 | ``` 85 | 86 | #### Ordinal 87 | ```go 88 | import "github.com/sbani/go-humanizer/numbers" 89 | 90 | Ordinal(0) // "th" 91 | Ordinal(1) // "st" 92 | Ordinal(2) // "nd" 93 | Ordinal(23) // "rd" 94 | Ordinal(1002) // "nd" 95 | Ordinal(-111) // "th" 96 | ``` 97 | #### Roman 98 | ```go 99 | import "github.com/sbani/go-humanizer/numbers" 100 | 101 | s, err := ToRoman(1) // "I" 102 | s, err := ToRomanToRoman(5) // "V" 103 | s, err := ToRomanToRoman(1300) // "MCCC" 104 | 105 | i, err := FromRoman("MMMCMXCIX") // 3999 106 | i, err := FromRoman("V") // 5 107 | i, err := FromRoman("CXXV") // 125 108 | ``` 109 | 110 | ### Collection 111 | 112 | #### Oxford 113 | ```go 114 | import "github.com/sbani/go-humanizer/collection" 115 | 116 | Oxford([]string{"Albert"}, -1) // "Albert" 117 | Oxford([]string{"Albert", "Norbert"}, -1) // "Albert and Norbert" 118 | Oxford([]string{"Albert", "Norbert", "Michael", "Kevin"}, -1) // "Albert, Norbert, Michael, and Kevin" 119 | Oxford([]string{"Albert", "Norbert", "Michael", "Kevin"}, 2)) // Albert, Norbert, and 2 more 120 | ``` 121 | 122 | ## License 123 | MIT License. See LICENSE file for more informations. 124 | 125 | ## Credits 126 | Special thank goes to [PHP Humanizer](https://github.com/coduo/php-humanizer). 127 | 128 | ## Contributions 129 | Contributions are very welcome! Feel free to contact me, send a PR or open an issue. 130 | 131 | ## Roadmap 132 | Things that are missing: 133 | - [x] Strings: Humanize 134 | - [x] Strings: Truncate 135 | - [x] Numbers: Roman 136 | - [x] Numbers: Ordinal 137 | - [x] Collection: Oxford 138 | - [x] Numbers: Binary Suffix 139 | - [x] Numbers: Metric Suffix 140 | - [x] Date time: Difference 141 | - [ ] Date time: Precise difference 142 | - [ ] Translations 143 | --------------------------------------------------------------------------------