diff --git a/Makefile.am b/Makefile.am
index f381ef23a..de02ebaa2 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -429,6 +429,7 @@ libutil_a_SOURCES = \
 	src/util/FormatString.cxx src/util/FormatString.hxx \
 	src/util/Tokenizer.cxx src/util/Tokenizer.hxx \
 	src/util/TextFile.hxx \
+	src/util/TimeParser.cxx src/util/TimeParser.hxx \
 	src/util/UriUtil.cxx src/util/UriUtil.hxx \
 	src/util/Manual.hxx \
 	src/util/RefCount.hxx \
diff --git a/src/SongFilter.cxx b/src/SongFilter.cxx
index 8919439ef..d2bbc9b32 100644
--- a/src/SongFilter.cxx
+++ b/src/SongFilter.cxx
@@ -25,9 +25,12 @@
 #include "util/ConstBuffer.hxx"
 #include "util/StringAPI.hxx"
 #include "util/ASCII.hxx"
+#include "util/TimeParser.hxx"
 #include "util/UriUtil.hxx"
 #include "lib/icu/Collate.hxx"
 
+#include <stdexcept>
+
 #include <assert.h>
 #include <stdlib.h>
 
@@ -180,24 +183,6 @@ SongFilter::~SongFilter()
 	/* this destructor exists here just so it won't get inlined */
 }
 
-#if !defined(__GLIBC__) && !defined(WIN32)
-
-/**
- * Determine the time zone offset in a portable way.
- */
-gcc_const
-static time_t
-GetTimeZoneOffset()
-{
-	time_t t = 1234567890;
-	struct tm tm;
-	tm.tm_isdst = 0;
-	gmtime_r(&t, &tm);
-	return t - mktime(&tm);
-}
-
-#endif
-
 gcc_pure
 static time_t
 ParseTimeStamp(const char *s)
@@ -210,26 +195,13 @@ ParseTimeStamp(const char *s)
 		/* it's an integral UNIX time stamp */
 		return (time_t)value;
 
-#ifdef WIN32
-	/* TODO: emulate strptime()? */
-	return 0;
-#else
-	/* try ISO 8601 */
-
-	struct tm tm;
-	const char *end = strptime(s, "%FT%TZ", &tm);
-	if (end == nullptr || *end != 0)
+	try {
+		/* try ISO 8601 */
+		const auto t = ParseTimePoint(s, "%FT%TZ");
+		return std::chrono::system_clock::to_time_t(t);
+	} catch (const std::runtime_error &) {
 		return 0;
-
-#ifdef __GLIBC__
-	/* timegm() is a GNU extension */
-	return timegm(&tm);
-#else
-	tm.tm_isdst = 0;
-	return mktime(&tm) + GetTimeZoneOffset();
-#endif /* !__GLIBC__ */
-
-#endif /* !WIN32 */
+	}
 }
 
 bool
diff --git a/src/util/TimeParser.cxx b/src/util/TimeParser.cxx
new file mode 100644
index 000000000..b42d93380
--- /dev/null
+++ b/src/util/TimeParser.cxx
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2014-2017 Max Kellermann <max@duempel.org>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - 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
+ * FOUNDATION 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.
+ */
+
+#include "TimeParser.hxx"
+
+#include <stdexcept>
+
+#include <assert.h>
+#include <time.h>
+
+#if !defined(__GLIBC__) && !defined(WIN32)
+
+/**
+ * Determine the time zone offset in a portable way.
+ */
+gcc_const
+static time_t
+GetTimeZoneOffset()
+{
+	time_t t = 1234567890;
+	struct tm tm;
+	tm.tm_isdst = 0;
+	gmtime_r(&t, &tm);
+	return t - mktime(&tm);
+}
+
+#endif
+
+std::chrono::system_clock::time_point
+ParseTimePoint(const char *s, const char *format)
+{
+	assert(s != nullptr);
+	assert(format != nullptr);
+
+#ifdef WIN32
+	/* TODO: emulate strptime()? */
+	throw std::runtime_error("Time parsing not implemented on Windows");
+#else
+	struct tm tm;
+	const char *end = strptime(s, format, &tm);
+	if (end == nullptr || *end != 0)
+		throw std::runtime_error("Failed to parse time stamp");
+
+#ifdef __GLIBC__
+	/* timegm() is a GNU extension */
+	const auto t = timegm(&tm);
+#else
+	tm.tm_isdst = 0;
+	const auto t = mktime(&tm) + GetTimeZoneOffset();
+#endif /* !__GLIBC__ */
+
+	return std::chrono::system_clock::from_time_t(t);
+
+#endif /* !WIN32 */
+}
diff --git a/src/util/TimeParser.hxx b/src/util/TimeParser.hxx
new file mode 100644
index 000000000..4c7c66a77
--- /dev/null
+++ b/src/util/TimeParser.hxx
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2014-2017 Max Kellermann <max@duempel.org>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - 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
+ * FOUNDATION 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.
+ */
+
+#ifndef TIME_PARSER_HXX
+#define TIME_PARSER_HXX
+
+#include <chrono>
+
+/**
+ * Parse a time stamp.
+ *
+ * Throws std::runtime_error on error.
+ */
+std::chrono::system_clock::time_point
+ParseTimePoint(const char *s, const char *format);
+
+#endif