Repository: stigok/ruterstop

Issue:
Mulighet for å vise klokkeslett
Det hadde vært kult man kunne vist klokkeslett hvis det var over en viss tid igjen. Slik ofte skilt pleier å være.

Discussion:

Patch:
diff --git a/ruterstop/__init__.py b/ruterstop/__init__.py
index 7e4028a..b44d6be 100644
--- a/ruterstop/__init__.py
+++ b/ruterstop/__init__.py
@@ -18,7 +18,7 @@ from datetime import datetime, timedelta
 import requests
 import bottle
 
-from ruterstop.utils import norwegian_ascii, timed_cache
+from ruterstop.utils import delta, human_delta, norwegian_ascii, timed_cache
 
 __version__ = "0.3.1"
 
@@ -82,32 +82,6 @@ def default_error_handler(res):
 
 webapp.default_error_handler = default_error_handler
 
-def human_delta(until=None, *, since=None):
-    """
-    Return a 6 char long string describing minutes left 'until' date occurs.
-    Example output:
-       naa (if delta < 60 seconds)
-     1 min
-     7 min
-    10 min
-    99 min
-    """
-    now_str = "{:>6}".format("naa")
-    if not since:
-        since = datetime.now()
-
-    if since > until:
-        return now_str
-
-    secs = (until - since).seconds
-    mins = int(secs / 60) # floor
-    mins = max(0, min(mins, 99)) # between 0 and 99
-
-    if mins < 1:
-        return now_str
-
-    return "{:2} min".format(mins)
-
 
 class Departure(namedtuple("Departure", ["line", "name", "eta", "direction", "realtime"])):
     """Represents a transport departure"""
@@ -117,6 +91,13 @@ class Departure(namedtuple("Departure", ["line", "name", "eta", "direction", "re
             name += " " + self.name
         return "{:14}{:>7}".format(name[:14], human_delta(until=self.eta))
 
+    def ts_str(self):
+        name = str(self.line)
+        if self.name:
+            name += " " + self.name
+        return "{:16}{:%H:%M}".format(name[:14], self.eta)
+
+
 # Python < 3.7 equivalent of `defaults` kwarg of `namedtuple`
 Departure.__new__.__defaults__ = (False,)
 
@@ -223,7 +204,7 @@ def get_departures(*, stop_id=None):
     return parse_departures(raw_stop)
 
 
-def format_departure_list(departures, *, min_eta=0, directions=None, grouped=False):
+def format_departure_list(departures, *, min_eta=0, long_eta=-1, directions=None, grouped=False):
     """
     Filters, formats and groups departures based on arguments passed.
     """
@@ -268,7 +249,10 @@ def format_departure_list(departures, *, min_eta=0, directions=None, grouped=Fal
     # Create pretty output
     s = ""
     for dep in deps:
-        s += str(dep) + '\n'
+        if long_eta >= 0 and delta(dep.eta) > long_eta:
+            s += dep.ts_str() + '\n'
+        else:
+            s += str(dep) + '\n'
     return s
 
 
@@ -285,6 +269,9 @@ def main(argv=sys.argv, *, stdout=sys.stdout):
                      help="filter direction of departures")
     par.add_argument('--min-eta', type=int, default=0, metavar="<minutes>",
                      help="minimum ETA of departures to return")
+    par.add_argument('--long-eta', type=int, default=-1, metavar="<minutes>",
+                     help="show departure time when ETA is later than this limit" +
+                          "(disable with -1)")
     par.add_argument('--grouped', action="store_true",
                      help="group departures with same ETA together " +
                           "when --direction is also specified.")
@@ -332,7 +319,9 @@ def main(argv=sys.argv, *, stdout=sys.stdout):
 
         # Just print stop information
         deps = get_departures(stop_id=args.stop_id)
-        formatted = format_departure_list(deps, min_eta=args.min_eta,
+        formatted = format_departure_list(deps,
+                                          min_eta=args.min_eta,
+                                          long_eta=args.long_eta,
                                           directions=directions,
                                           grouped=args.grouped)
 
diff --git a/ruterstop/utils.py b/ruterstop/utils.py
index 77f762f..4852322 100644
--- a/ruterstop/utils.py
+++ b/ruterstop/utils.py
@@ -38,3 +38,40 @@ def timed_cache(*, expires_sec=60, now=datetime.now):
         return wrapper
 
     return decorator
+
+
+def delta(until=None, *, since=None):
+    """
+    Return amount of whole minutes until `until` date occurs, since `since`.
+    Returns -1 if since is later than until.
+    """
+    if not since:
+        since = datetime.now()
+
+    if since > until:
+        return -1
+
+    secs = (until - since).seconds
+    mins = max(0, secs / 60)
+    return int(mins)
+
+
+def human_delta(until=None, *, since=None):
+    """
+    Return a 6 char long string describing minutes left 'until' date occurs.
+    Example output:
+       naa (if delta < 60 seconds)
+     1 min
+     7 min
+    10 min
+    99 min
+    """
+    now_str = "{:>6}".format("naa")
+
+    since = since if since else datetime.now()
+    mins = min(delta(until, since=since), 99)
+
+    if mins < 1:
+        return now_str
+
+    return "{:2} min".format(mins)


Test patch:
diff --git a/ruterstop/tests/test_ruterstop.py b/ruterstop/tests/test_ruterstop.py
index b261318..976b579 100644
--- a/ruterstop/tests/test_ruterstop.py
+++ b/ruterstop/tests/test_ruterstop.py
@@ -6,12 +6,14 @@ from io import StringIO
 from unittest import TestCase
 from unittest.mock import Mock, MagicMock, patch
 
+from freezegun import freeze_time
+
 import ruterstop
 
 
 class DepartureClassTestCase(TestCase):
     def test_str_representation(self):
-        with patch('ruterstop.datetime') as mock_date:
+        with patch('ruterstop.utils.datetime') as mock_date:
             ref = datetime.min
             mock_date.now.return_value = ref
             in_7_mins = ref + timedelta(minutes=7)
@@ -141,3 +143,29 @@ class RuterstopTestCase(TestCase):
             self.assertEqual(lines[1], "20, 21          2 min")
             self.assertEqual(lines[2], "21 Thre         3 min")
             self.assertEqual(lines[3], "21 Four         4 min")
+
+    @patch("ruterstop.get_realtime_stop", return_value=None)
+    def test_shows_timestamp_for_long_etas(self, _):
+        seed = datetime(2020, 1, 1, 10, 0, 0)
+        with freeze_time(seed):
+            def futr(minutes):
+                return seed + timedelta(minutes=minutes)
+
+            d = ruterstop.Departure
+            deps = [
+                # Shouldn't matter if a departure is realtime or not
+                d("01", "a", futr(60),  "inbound", realtime=True),
+                d("02", "b", futr(120), "inbound", realtime=True),
+                d("03", "c", futr(150), "inbound", realtime=False),
+            ]
+
+            args = " --stop-id=2121 --direction=inbound --long-eta=59".split(' ')
+
+            # Use the fake departure list in this patch
+            with patch("ruterstop.parse_departures", return_value=deps) as mock:
+                with StringIO() as output:
+                    ruterstop.main(args, stdout=output)
+                    lines = output.getvalue().split('\n')
+                self.assertEqual(lines[0], "01 a            11:00")
+                self.assertEqual(lines[1], "02 b            12:00")
+                self.assertEqual(lines[2], "03 c            12:30")
diff --git a/ruterstop/tests/test_utils.py b/ruterstop/tests/test_utils.py
index 98b2420..53a5786 100644
--- a/ruterstop/tests/test_utils.py
+++ b/ruterstop/tests/test_utils.py
@@ -24,7 +24,7 @@ class HumanDeltaTestCase(TestCase):
             self.assertEqual(res, expected, "test case #%d" % (i + 1))
 
     def test_default_kwarg_value(self):
-        with patch('ruterstop.datetime') as mock_date:
+        with patch('ruterstop.utils.datetime') as mock_date:
             mock_date.now.return_value = datetime.min
             ruterstop.human_delta(until=datetime.min + timedelta(seconds=120))
             self.assertEqual(mock_date.now.call_count, 1)