├── .travis.yml ├── Makefile ├── cmd └── timeago │ └── main.go ├── LICENSE ├── README.md ├── timeago.go └── timeago_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | sudo: false 4 | 5 | go: 6 | - 1.5 7 | - 1.6 8 | - tip 9 | 10 | matrix: 11 | allow_failures: 12 | - go: tip 13 | 14 | install: 15 | - go get -t 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: test lint vet 2 | 3 | test: dependencies 4 | go test ./... 5 | 6 | lint: dependencies 7 | @which golint || go get github.com/golang/lint/golint 8 | golint ./... 9 | 10 | vet: dependencies 11 | go vet ./... 12 | 13 | dependencies: 14 | go get -t 15 | 16 | .PHONY: dependencies lint test 17 | -------------------------------------------------------------------------------- /cmd/timeago/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "time" 8 | 9 | "github.com/justincampbell/timeago" 10 | ) 11 | 12 | func main() { 13 | flag.Usage = usage 14 | flag.Parse() 15 | input := flag.Arg(0) 16 | 17 | if input == "help" { 18 | usage() 19 | os.Exit(0) 20 | } 21 | 22 | if input == "" { 23 | usage() 24 | os.Exit(1) 25 | } 26 | 27 | d, err := time.ParseDuration(input) 28 | if err != nil { 29 | fmt.Printf("%s\n\n", err) 30 | usage() 31 | os.Exit(1) 32 | } 33 | fmt.Println(timeago.FromDuration(d)) 34 | } 35 | 36 | func usage() { 37 | fmt.Println("github.com/justincampbell/timeago") 38 | fmt.Println("usage: timeago DURATION") 39 | fmt.Println("A duration can be in s (seconds), m (minutes), or h (hours)") 40 | fmt.Println("Examples: 5s 1m30s 4h30m") 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Justin Campbell 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # timeago 2 | [![Build Status](https://travis-ci.org/justincampbell/timeago.svg?branch=master)](https://travis-ci.org/justincampbell/timeago) 3 | [![GoDoc](https://godoc.org/github.com/justincampbell/timeago?status.svg)](https://godoc.org/github.com/justincampbell/timeago) 4 | 5 | A port of Rails' `time_ago_in_words` to Golang 6 | 7 | ## Installation 8 | 9 | ``` 10 | go get github.com/justincampbell/timeago 11 | ``` 12 | 13 | ## Examples 14 | 15 | ### Golang 16 | 17 | ```go 18 | package main 19 | 20 | import ( 21 | "fmt" 22 | "time" 23 | 24 | "github.com/justincampbell/timeago" 25 | ) 26 | 27 | func main() { 28 | var d time.Duration 29 | 30 | d, _ = time.ParseDuration("25s") 31 | fmt.Println(timeago.FromDuration(d)) 32 | d, _ = time.ParseDuration("55m") 33 | fmt.Println(timeago.FromDuration(d)) 34 | d, _ = time.ParseDuration("72h") 35 | fmt.Println(timeago.FromDuration(d)) 36 | } 37 | ``` 38 | 39 | ``` 40 | $ go run main.go 41 | less than a minute 42 | about 1 hour 43 | 3 days 44 | ``` 45 | 46 | ### Command-line 47 | 48 | ``` 49 | $ go get github.com/justincampbell/timeago/cmd/timeago 50 | $ timeago 25s 51 | less than a minute 52 | $ timeago 55m 53 | about 1 hour 54 | $ timeago 72h 55 | 3 days 56 | ``` 57 | -------------------------------------------------------------------------------- /timeago.go: -------------------------------------------------------------------------------- 1 | package timeago 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "math" 7 | "time" 8 | ) 9 | 10 | const ( 11 | minute = 1 12 | hour = minute * 60 13 | day = hour * 24 14 | month = day * 30 15 | year = day * 365 16 | quarter = year / 4 17 | ) 18 | 19 | // FromDuration returns a friendly string representing an approximation of the 20 | // given duration 21 | func FromDuration(d time.Duration) string { 22 | seconds := round(d.Seconds()) 23 | 24 | if seconds < 30 { 25 | return "less than a minute" 26 | } 27 | 28 | if seconds < 90 { 29 | return "1 minute" 30 | } 31 | 32 | minutes := div(seconds, 60) 33 | 34 | if minutes < 45 { 35 | return fmt.Sprintf("%0d minutes", minutes) 36 | } 37 | 38 | hours := div(minutes, 60) 39 | 40 | if minutes < day { 41 | return fmt.Sprintf("about %s", pluralize(hours, "hour")) 42 | } 43 | 44 | if minutes < (42 * hour) { 45 | return "1 day" 46 | } 47 | 48 | days := div(hours, 24) 49 | 50 | if minutes < (30 * day) { 51 | return pluralize(days, "day") 52 | } 53 | 54 | months := div(days, 30) 55 | 56 | if minutes < (45 * day) { 57 | return "about 1 month" 58 | } 59 | 60 | if minutes < (60 * day) { 61 | return "about 2 months" 62 | } 63 | 64 | if minutes < year { 65 | return pluralize(months, "month") 66 | } 67 | 68 | rem := minutes % year 69 | years := minutes / year 70 | 71 | if rem < (3 * month) { 72 | return fmt.Sprintf("about %s", pluralize(years, "year")) 73 | } 74 | if rem < (9 * month) { 75 | return fmt.Sprintf("over %s", pluralize(years, "year")) 76 | } 77 | 78 | years++ 79 | return fmt.Sprintf("almost %s", pluralize(years, "year")) 80 | } 81 | 82 | // FromTime returns a friendly string representing the approximate difference 83 | // from the given time and time.Now() 84 | func FromTime(t time.Time) string { 85 | now := time.Now() 86 | 87 | var d time.Duration 88 | var suffix string 89 | 90 | if t.Before(now) { 91 | d = now.Sub(t) 92 | suffix = "ago" 93 | } else { 94 | d = t.Sub(now) 95 | suffix = "from now" 96 | } 97 | 98 | return fmt.Sprintf("%s %s", FromDuration(d), suffix) 99 | } 100 | 101 | func pluralize(i int, s string) string { 102 | var buf bytes.Buffer 103 | buf.WriteString(fmt.Sprintf("%d %s", i, s)) 104 | if i != 1 { 105 | buf.WriteString("s") 106 | } 107 | return buf.String() 108 | } 109 | 110 | func round(f float64) int { 111 | return int(math.Floor(f + .50)) 112 | } 113 | 114 | func div(numerator int, denominator int) int { 115 | rem := numerator % denominator 116 | result := numerator / denominator 117 | 118 | if rem >= (denominator / 2) { 119 | result++ 120 | } 121 | 122 | return result 123 | } 124 | -------------------------------------------------------------------------------- /timeago_test.go: -------------------------------------------------------------------------------- 1 | package timeago 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/justincampbell/bigduration" 8 | ) 9 | 10 | func Test_FromDuration(t *testing.T) { 11 | cases := map[string]string{ 12 | "0s": "less than a minute", 13 | "29s": "less than a minute", 14 | "30s": "1 minute", 15 | "1m29s": "1 minute", 16 | "1m30s": "2 minutes", 17 | "44m29s": "44 minutes", 18 | "44m30s": "about 1 hour", 19 | "89m29s": "about 1 hour", 20 | "89m30s": "about 2 hours", 21 | "23h59m29s": "about 24 hours", 22 | "23h59m30s": "1 day", 23 | "41h59m29s": "1 day", 24 | "41h59m30s": "2 days", 25 | "2D12h": "3 days", 26 | "29D23h59m29s": "30 days", 27 | "29D23h59m30s": "about 1 month", 28 | "44D23h59m29s": "about 1 month", 29 | "44D23h59m30s": "about 2 months", 30 | "59D23h59m29s": "about 2 months", 31 | "59D23h59m30s": "2 months", 32 | "364D23h59m29s": "12 months", 33 | "364D23h59m30s": "about 1 year", 34 | "1Y89D": "about 1 year", 35 | "1Y6M": "over 1 year", 36 | "1Y276D": "almost 2 years", 37 | "2Y89D": "about 2 years", 38 | "2Y90D": "over 2 years", 39 | "2Y269D": "over 2 years", 40 | "2Y9M": "almost 3 years", 41 | "4Y276D": "almost 5 years", 42 | "5Y89D": "about 5 years", 43 | "5Y3M": "over 5 years", 44 | "5Y8M29D": "over 5 years", 45 | "5Y9M": "almost 6 years", 46 | "9Y9M": "almost 10 years", 47 | "10Y89D": "about 10 years", 48 | "10Y3M": "over 10 years", 49 | "10Y8M29D": "over 10 years", 50 | "10Y9M": "almost 11 years", 51 | } 52 | 53 | for input, expected := range cases { 54 | d, err := bigduration.ParseDuration(input) 55 | if err != nil { 56 | t.Fatal(err) 57 | } 58 | 59 | if v := FromDuration(d); v != expected { 60 | t.Fatalf("for %#v, expected %#v, but got %#v", input, expected, v) 61 | } 62 | } 63 | } 64 | 65 | func Test_FromTime(t *testing.T) { 66 | expected := "less than a minute ago" 67 | if v := FromTime(time.Now()); v != expected { 68 | t.Fatalf("expected %#v, but got %#v", expected, v) 69 | } 70 | 71 | futureCases := map[string]string{ 72 | "29s": "less than a minute from now", 73 | "45m": "about 1 hour from now", 74 | "2D12h": "3 days from now", 75 | } 76 | 77 | for input, expected := range futureCases { 78 | d, err := bigduration.ParseDuration(input) 79 | if err != nil { 80 | t.Fatal(err) 81 | } 82 | 83 | if v := FromTime(time.Now().Add(d)); v != expected { 84 | t.Fatalf("for %#v, expected %#v, but got %#v", input, expected, v) 85 | } 86 | } 87 | 88 | pastCases := map[string]string{ 89 | "29s": "less than a minute ago", 90 | "45m": "about 1 hour ago", 91 | "2D12h": "3 days ago", 92 | } 93 | 94 | for input, expected := range pastCases { 95 | d, err := bigduration.ParseDuration(input) 96 | if err != nil { 97 | t.Fatal(err) 98 | } 99 | 100 | if v := FromTime(time.Now().Add(-d)); v != expected { 101 | t.Fatalf("for %#v, expected %#v, but got %#v", input, expected, v) 102 | } 103 | } 104 | } 105 | --------------------------------------------------------------------------------