diff --git a/lib/Travelynx.pm b/lib/Travelynx.pm
index 32b6b6d7f1f53e68aa6e7512ad381923342b86c7..a5dbcb828c4a7c235432f76f6dbe9589da3f29aa 100755
--- a/lib/Travelynx.pm
+++ b/lib/Travelynx.pm
@@ -2781,7 +2781,7 @@ sub startup {
 				}
 				$next_departure = $journey->{rt_dep_ts};
 			}
-			return {
+			my $ret = {
 				km_route             => $km_route,
 				km_beeline           => $km_beeline,
 				num_trains           => $num_trains,
@@ -2793,6 +2793,21 @@ sub startup {
 				delay_arr            => $delay_arr,
 				inconsistencies      => \@inconsistencies,
 			};
+			for my $key (
+				qw(min_travel_sched min_travel_real min_interchange_real delay_dep delay_arr)
+			  )
+			{
+				my $strf_key = $key . '_strf';
+				my $value    = $ret->{$key};
+				$ret->{$strf_key} = q{};
+				if ( $ret->{$key} < 0 ) {
+					$ret->{$strf_key} .= '-';
+					$value *= -1;
+				}
+				$ret->{$strf_key}
+				  .= sprintf( '%02d:%02d', $value / 60, $value % 60 );
+			}
+			return $ret;
 		}
 	);
 
diff --git a/lib/Travelynx/Command/database.pm b/lib/Travelynx/Command/database.pm
index e92dd4b14d91126e0f9efa6b59cd4938391f08e7..a257b8ba70483be7eb39cb5d45935e5805e774cc 100644
--- a/lib/Travelynx/Command/database.pm
+++ b/lib/Travelynx/Command/database.pm
@@ -1038,6 +1038,20 @@ my @migrations = (
 			}
 		);
 	},
+
+	# v22 -> v23
+	# 1.18.1 fixes handling of negative cumulative arrival/departure delays
+	# and introduces additional statistics entries with pre-formatted duration
+	# strings while at it. Old cache entries lack those.
+	sub {
+		my ($db) = @_;
+		$db->query(
+			qq{
+				truncate journey_stats;
+				update schema_version set version = 23;
+			}
+		);
+	},
 );
 
 sub setup_db {
diff --git a/t/r-negative-delay.t b/t/r-negative-delay.t
new file mode 100644
index 0000000000000000000000000000000000000000..a2818c5b8eee08efd8e723268acd7331760d0977
--- /dev/null
+++ b/t/r-negative-delay.t
@@ -0,0 +1,94 @@
+#!/usr/bin/env perl
+use Mojo::Base -strict;
+
+# Regression test: handle negative cumulative arrival / departure delay
+
+use Test::More;
+use Test::Mojo;
+
+# Include application
+use FindBin;
+require "$FindBin::Bin/../index.pl";
+
+my $t = Test::Mojo->new('Travelynx');
+
+if ( not $t->app->config->{db} ) {
+	plan( skip_all => 'No database configured' );
+}
+
+$t->app->pg->db->query(
+	'drop schema if exists travelynx_regr_negative_delay cascade');
+$t->app->pg->db->query('create schema travelynx_regr_negative_delay');
+$t->app->pg->db->query('set search_path to travelynx_regr_negative_delay');
+$t->app->pg->on(
+	connection => sub {
+		my ( $pg, $dbh ) = @_;
+		$dbh->do('set search_path to travelynx_regr_negative_delay');
+	}
+);
+
+$t->app->config->{mail}->{disabled} = 1;
+
+$t->app->start( 'database', 'migrate' );
+
+my $csrf_token
+  = $t->ua->get('/register')->res->dom->at('input[name=csrf_token]')
+  ->attr('value');
+
+# Successful registration
+$t->post_ok(
+	'/register' => form => {
+		csrf_token => $csrf_token,
+		user       => 'someone',
+		email      => 'foo@example.org',
+		password   => 'foofoofoo',
+		password2  => 'foofoofoo',
+	}
+);
+$t->status_is(200)->content_like(qr{Verifizierungslink});
+
+my $res = $t->app->pg->db->select( 'users', ['id'], { name => 'someone' } );
+my $uid = $res->hash->{id};
+$res = $t->app->pg->db->select( 'pending_registrations', ['token'],
+	{ user_id => $uid } );
+my $token = $res->hash->{token};
+
+# Successful verification
+$t->get_ok("/reg/${uid}/${token}");
+$t->status_is(200)->content_like(qr{freigeschaltet});
+
+# Successful login
+$t->post_ok(
+	'/login' => form => {
+		csrf_token => $csrf_token,
+		user       => 'someone',
+		password   => 'foofoofoo',
+	}
+);
+$t->status_is(302)->header_is( location => '/' );
+
+$csrf_token
+  = $t->ua->get('/journey/add')->res->dom->at('input[name=csrf_token]')
+  ->attr('value');
+$t->post_ok(
+	'/journey/add' => form => {
+		csrf_token      => $csrf_token,
+		action          => 'save',
+		train           => 'RE 42 11238',
+		dep_station     => 'EMST',
+		sched_departure => '16.10.2018 17:36',
+		rt_departure    => '16.10.2018 17:35',
+		arr_station     => 'EG',
+		sched_arrival   => '16.10.2018 18:34',
+		rt_arrival      => '16.10.2018 18:32',
+	}
+);
+$t->status_is(302)->header_is( location => '/journey/1' );
+
+$t->get_ok('/history/2018/10')->status_is(200)->content_like(qr{62 km})
+  ->content_like(qr{00:57 Stunden})->content_like(qr{nach Fahrplan: 00:58})
+  ->content_like(qr{Bei Abfahrt: -00:01 Stunden})
+  ->content_like(qr{Bei Ankunft: -00:02 Stunden});
+
+$t->app->pg->db->query('drop schema travelynx_regr_negative_delay cascade');
+done_testing();
diff --git a/templates/_history_stats.html.ep b/templates/_history_stats.html.ep
index 8197ed13916fd2200ddb9d44d7828b89e393443e..d6c7979bff655ca26267bc28723b733364f30dea 100644
--- a/templates/_history_stats.html.ep
+++ b/templates/_history_stats.html.ep
@@ -39,17 +39,17 @@
 			</tr>
 			<tr>
 				<th scope="row">Fahrtzeit</th>
-				<td><%= sprintf('%02d:%02d', $stats->{min_travel_real} / 60, $stats->{min_travel_real} % 60) %> Stunden
-					(nach Fahrplan: <%= sprintf('%02d:%02d', $stats->{min_travel_sched} / 60, $stats->{min_travel_sched} % 60) %>)<td>
+				<td><%= $stats->{min_travel_real_strf} %> Stunden
+					(nach Fahrplan: <%= $stats->{min_travel_sched_strf} %>)<td>
 			</tr>
 			<tr>
 				<th scope="row">Wartezeit (nur Umstiege)</th>
-				<td><%= sprintf('%02d:%02d', $stats->{min_interchange_real} / 60, $stats->{min_interchange_real} % 60) %> Stunden
+				<td><%= $stats->{min_interchange_real_strf} %> Stunden
 			</tr>
 			<tr>
 				<th scope="row">Kumulierte Verspätung</th>
-				<td>Bei Abfahrt: <%= sprintf('%02d:%02d', $stats->{delay_dep} / 60, $stats->{delay_dep} % 60) %> Stunden<br/>
-					Bei Ankunft: <%= sprintf('%02d:%02d', $stats->{delay_arr} / 60, $stats->{delay_arr} % 60) %> Stunden</td>
+				<td>Bei Abfahrt: <%= $stats->{delay_dep_strf} %> Stunden<br/>
+					Bei Ankunft: <%= $stats->{delay_arr_strf} %> Stunden</td>
 			</tr>
 		</table>
 	</div>