ISO8601.cxx 5.22 KB
Newer Older
1
/*
2
 * Copyright 2007-2019 Content Management AG
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
 * All rights reserved.
 *
 * author: Max Kellermann <mk@cm4all.com>
 *
 * 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.
 */

33 34
#include "ISO8601.hxx"
#include "Convert.hxx"
35
#include "util/StringBuffer.hxx"
36

37
#include <cassert>
38
#include <cstdlib>
39 40
#include <stdexcept>

41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
StringBuffer<64>
FormatISO8601(const struct tm &tm) noexcept
{
	StringBuffer<64> buffer;
	strftime(buffer.data(), buffer.capacity(),
#ifdef _WIN32
		 "%Y-%m-%dT%H:%M:%SZ",
#else
		 "%FT%TZ",
#endif
		 &tm);
	return buffer;
}

StringBuffer<64>
FormatISO8601(std::chrono::system_clock::time_point tp)
{
	return FormatISO8601(GmTime(tp));
}

61 62
#ifndef _WIN32

63 64 65 66
static std::pair<unsigned, unsigned>
ParseTimeZoneOffsetRaw(const char *&s)
{
	char *endptr;
67
	unsigned long value = std::strtoul(s, &endptr, 10);
68 69 70 71 72 73 74 75 76
	if (endptr == s + 4) {
		s = endptr;
		return std::make_pair(value / 100, value % 100);
	} else if (endptr == s + 2) {
		s = endptr;

		unsigned hours = value, minutes = 0;
		if (*s == ':') {
			++s;
77
			minutes = std::strtoul(s, &endptr, 10);
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112
			if (endptr != s + 2)
				throw std::runtime_error("Failed to parse time zone offset");

			s = endptr;
		}

		return std::make_pair(hours, minutes);
	} else
		throw std::runtime_error("Failed to parse time zone offset");
}

static std::chrono::system_clock::duration
ParseTimeZoneOffset(const char *&s)
{
	assert(*s == '+' || *s == '-');

	bool negative = *s == '-';
	++s;

	auto raw = ParseTimeZoneOffsetRaw(s);
	if (raw.first > 13)
		throw std::runtime_error("Time offset hours out of range");

	if (raw.second >= 60)
		throw std::runtime_error("Time offset minutes out of range");

	std::chrono::system_clock::duration d = std::chrono::hours(raw.first);
	d += std::chrono::minutes(raw.second);

	if (negative)
		d = -d;

	return d;
}

113 114 115 116
static const char *
ParseTimeOfDay(const char *s, struct tm &tm,
	       std::chrono::system_clock::duration &precision) noexcept
{
117 118 119 120
	/* this function always checks "end==s" to work around a
	   strptime() bug on OS X: if nothing could be parsed,
	   strptime() returns the input string (indicating success)
	   instead of nullptr (indicating error) */
121

122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138
	const char *end = strptime(s, "%H", &tm);
	if (end == nullptr || end == s)
		return end;

	s = end;
	precision = std::chrono::hours(1);

	if (*s == ':') {
		/* with field separators: now a minute must follow */

		++s;

		end = strptime(s, "%M", &tm);
		if (end == nullptr || end == s)
			return nullptr;

		s = end;
139 140
		precision = std::chrono::minutes(1);

141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
		/* the "seconds" field is optional */
		if (*s != ':')
			return s;

		++s;

		end = strptime(s, "%S", &tm);
		if (end == nullptr || end == s)
			return nullptr;

		precision = std::chrono::seconds(1);
		return end;
	}

	/* without field separators */

	end = strptime(s, "%M", &tm);
	if (end == nullptr || end == s)
		return s;

	s = end;
	precision = std::chrono::minutes(1);

	end = strptime(s, "%S", &tm);
	if (end == nullptr || end == s)
		return s;

	precision = std::chrono::seconds(1);
169 170 171
	return end;
}

172 173
#endif

174 175
std::pair<std::chrono::system_clock::time_point,
	  std::chrono::system_clock::duration>
176 177
ParseISO8601(const char *s)
{
178 179 180 181 182 183 184 185 186
	assert(s != nullptr);

#ifdef _WIN32
	/* TODO: emulate strptime()? */
	(void)s;
	throw std::runtime_error("Time parsing not implemented on Windows");
#else
	struct tm tm{};

187 188
	/* parse the date */
	const char *end = strptime(s, "%F", &tm);
189 190 191 192 193 194
	if (end == nullptr) {
		/* try without field separators */
		end = strptime(s, "%Y%m%d", &tm);
		if (end == nullptr)
			throw std::runtime_error("Failed to parse date");
	}
195 196 197 198 199 200 201 202

	s = end;

	std::chrono::system_clock::duration precision = std::chrono::hours(24);

	/* parse the time of day */
	if (*s == 'T') {
		++s;
203

204 205
		s = ParseTimeOfDay(s, tm, precision);
		if (s == nullptr)
206 207
			throw std::runtime_error("Failed to parse time of day");
	}
208 209 210

	auto tp = TimeGm(tm);

211
	/* time zone */
212 213
	if (*s == 'Z')
		++s;
214 215
	else if (*s == '+' || *s == '-')
		tp -= ParseTimeZoneOffset(s);
216

217 218 219
	if (*s != 0)
		throw std::runtime_error("Garbage at end of time stamp");

220
	return std::make_pair(tp, precision);
221
#endif /* !_WIN32 */
222
}