A CONTRIBUTING.md => CONTRIBUTING.md +31 -0
@@ 0,0 1,31 @@
+## Submitting Patches
+
+To submit a patch, first learn to use `git send-email` by reading
+[git-send-email.io], then read the SourceHut [mailing list etiquette] guide.
+You can send patches to my general purpose patches [mailing list].
+
+Please prefix the subject with `[PATCH reltime]`.
+To configure your checkout of this repo to always use the correct prefix and
+send to the correct list cd into the repo and run:
+
+ git config sendemail.to ~samwhited/patches@lists.sr.ht
+ git config format.subjectPrefix 'PATCH reltime'
+
+[git-send-email.io]: https://git-send-email.io/
+[mailing list etiquette]: https://man.sr.ht/lists.sr.ht/etiquette.md
+[mailing list]: https://lists.sr.ht/~samwhited/patches
+
+
+## License
+
+Licensed under the BSD 2 Clause License ([LICENSE] or
+https://opensource.org/licenses/BSD-2-Clause)
+
+[LICENSE]: https://git.sr.ht/~samwhited/reltime/tree/master/LICENSE.md
+
+
+### Contribution
+
+Unless you explicitly state otherwise, any contribution intentionally submitted
+for inclusion in the work by you shall be licensed as above, without any
+additional terms or conditions.
A LICENSE => LICENSE +23 -0
@@ 0,0 1,23 @@
+Copyright © 2014 The Soquee Contributors.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
A README.md => README.md +21 -0
@@ 0,0 1,21 @@
+# reltime
+
+Package **`reltime`** implements a "time ago" algorithm.
+
+```go
+import (
+ "code.soquee.net/reltime"
+)
+```
+
+
+## License
+
+The package may be used under the terms of the BSD 2-Clause License a copy of
+which may be found in the [`LICENSE`] file.
+
+Unless you explicitly state otherwise, any contribution submitted for inclusion
+in the work by you shall be licensed as above, without any additional terms or
+conditions.
+
+[`LICENSE`]: ./LICENSE
A go.mod => go.mod +3 -0
@@ 0,0 1,3 @@
+module code.soquee.net/reltime
+
+go 1.11
A reltime.go => reltime.go +56 -0
@@ 0,0 1,56 @@
+// Package reltime implements a "time ago" algorithm.
+package reltime // import "code.soquee.net/reltime"
+
+import (
+ "math"
+ "strconv"
+ "time"
+)
+
+// TimeAgo transforms the difference between a time and the current time into a
+// human readable string.
+//
+// It is a convenience wrapper for Ago(time.Until(t)).
+// For more information see the example on Ago.
+func TimeAgo(t time.Time) string {
+ return Ago(time.Until(t))
+}
+
+// Ago transforms durations into human readable strings.
+func Ago(d time.Duration) string {
+ // Take the absolute value and record the sign.
+ sign := d >> 63
+ d = (d ^ sign) - sign
+ var ago string
+ if sign < 0 {
+ ago = " ago"
+ }
+
+ switch {
+ case d < 30*time.Second:
+ return "just now"
+ case d < time.Minute:
+ return "less than a minute" + ago
+ case d < time.Minute+(30*time.Second):
+ return "about a minute" + ago
+ case d < 30*time.Minute:
+ return strconv.FormatFloat(math.Round(d.Minutes()), 'f', -1, 64) + " minutes" + ago
+ case d < time.Hour:
+ return "less than an hour" + ago
+ case d < time.Hour+30*time.Minute:
+ return "about an hour" + ago
+ case d < 24*time.Hour:
+ return strconv.FormatFloat(math.Round(d.Hours()), 'f', -1, 64) + " hours" + ago
+ case d < 32*time.Hour:
+ return "about a day" + ago
+ case d < 28*24*time.Hour:
+ return strconv.FormatFloat(math.Round(d.Hours()/24), 'f', -1, 64) + " days" + ago
+ case d < 45*24*time.Hour:
+ return "about a month" + ago
+ case d < 12*30*24*time.Hour:
+ return strconv.FormatFloat(math.Round(d.Hours()/24/30), 'f', -1, 64) + " months" + ago
+ case d < 18*30*24*time.Hour:
+ return "about a year" + ago
+ }
+ return strconv.FormatFloat(math.Round(d.Hours()/24/30/12), 'f', -1, 64) + " years" + ago
+}
A reltime_example_test.go => reltime_example_test.go +14 -0
@@ 0,0 1,14 @@
+package reltime
+
+import (
+ "fmt"
+ "time"
+)
+
+func ExampleAgo() {
+ s := Ago(-24 * time.Hour)
+ fmt.Println(s)
+
+ // Output:
+ // about a day ago
+}
A reltime_test.go => reltime_test.go +51 -0
@@ 0,0 1,51 @@
+package reltime_test
+
+import (
+ "strconv"
+ "testing"
+ "time"
+
+ "code.soquee.net/reltime"
+)
+
+var testCases = [...]struct {
+ in time.Duration
+ out string
+}{
+ 0: {
+ in: 15 * time.Second,
+ out: "just now",
+ },
+ 1: {in: (-9 * 24 * time.Hour) - (12 * time.Hour), out: "10 days ago"},
+ 2: {in: 15 * time.Second, out: "just now"},
+ 3: {in: 45 * time.Second, out: "less than a minute"},
+ 4: {in: -time.Minute, out: "about a minute ago"},
+ 5: {in: -3*time.Minute + (28 * time.Second), out: "3 minutes ago"},
+ 6: {in: 29 * time.Minute, out: "29 minutes"},
+ 7: {in: 39 * time.Minute, out: "less than an hour"},
+ 8: {in: -time.Hour, out: "about an hour ago"},
+ 9: {in: -2 * time.Hour, out: "2 hours ago"},
+ 10: {in: 25 * time.Hour, out: "about a day"},
+ 11: {in: 3 * 24 * time.Hour, out: "3 days"},
+ 12: {in: 25 * 24 * time.Hour, out: "25 days"},
+ 13: {in: 30 * 24 * time.Hour, out: "about a month"},
+ 14: {in: -7 * 30 * 24 * time.Hour, out: "7 months ago"},
+ 15: {in: 10 * 30 * 24 * time.Hour, out: "10 months"},
+ 16: {in: 12 * 30 * 24 * time.Hour, out: "about a year"},
+ 17: {in: -19 * 30 * 24 * time.Hour, out: "2 years ago"},
+ 18: {in: -20 * 12 * 30 * 24 * time.Hour, out: "20 years ago"},
+ 19: {in: 100 * 12 * 30 * 24 * time.Hour, out: "100 years"},
+ 20: {out: "just now"},
+ 21: {in: 10 * 31 * 24 * time.Hour, out: "10 months"},
+}
+
+func TestAgo(t *testing.T) {
+ for i, tc := range testCases {
+ t.Run(strconv.Itoa(i), func(t *testing.T) {
+ out := reltime.Ago(tc.in)
+ if out != tc.out {
+ t.Errorf("want=%q, got=%q", tc.out, out)
+ }
+ })
+ }
+}