--- /dev/null
+! Copyright (C) 2010 John Benediktsson
+! See http://factorcode.org/license.txt for BSD license
+
+USING: calendar calendar.elapsed kernel tools.test ;
+
+IN: calendar.elapsed.test
+
+[ -1 elapsed-time ] [ "negative seconds" = ] must-fail-with
+
+{ "0s" } [ 0 elapsed-time ] unit-test
+{ "59s" } [ 59 elapsed-time ] unit-test
+{ "1m" } [ 60 elapsed-time ] unit-test
+{ "1m 1s" } [ 61 elapsed-time ] unit-test
+{ "2y 1w 6d 2h 59m 23s" } [ 64033163 elapsed-time ] unit-test
+
+[ -1 relative-time ] [ "negative seconds" = ] must-fail-with
+
+{ "just now" } [ 0 relative-time ] unit-test
+{ "less than a minute ago" } [ 10 relative-time ] unit-test
+{ "about a minute ago" } [ 60 relative-time ] unit-test
+{ "about a minute ago" } [ 90 relative-time ] unit-test
+{ "4 minutes ago" } [ 270 relative-time ] unit-test
--- /dev/null
+! Copyright (C) 2010 John Benediktsson
+! See http://factorcode.org/license.txt for BSD license
+
+USING: combinators formatting kernel make math math.parser
+sequences ;
+
+IN: calendar.elapsed
+
+: elapsed-time ( seconds -- string )
+ dup 0 < [ "negative seconds" throw ] when [
+ {
+ { 60 "s" }
+ { 60 "m" }
+ { 24 "h" }
+ { 7 "d" }
+ { 52 "w" }
+ { f "y" }
+ } [
+ [ first [ /mod ] [ dup ] if* ] [ second ] bi swap
+ dup 0 > [ number>string prepend , ] [ 2drop ] if
+ ] each drop
+ ] { } make [ "0s" ] [ reverse " " join ] if-empty ;
+
+: relative-time ( seconds -- string )
+ dup 0 < [ "negative seconds" throw ] when {
+ { [ dup 1 < ] [ drop "just now" ] }
+ { [ dup 60 < ] [ drop "less than a minute ago" ] }
+ { [ dup 120 < ] [ drop "about a minute ago" ] }
+ { [ dup 2700 < ] [ 60 / "%d minutes ago" sprintf ] }
+ { [ dup 5400 < ] [ drop "about an hour ago" ] }
+ { [ dup 86400 < ] [ 3600 / "%d hours ago" sprintf ] }
+ { [ dup 172800 < ] [ drop "1 day ago" ] }
+ [ 86400 / "%d days ago" sprintf ]
+ } cond ;