blob: e038eee8f15d4f606f583b3e61aedfd89f51a1a9 [file] [log] [blame]
// Copyright (c) 2019, Google Inc. Please see the AUTHORS file for details.
// All rights reserved. Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
import 'package:built_collection/built_collection.dart';
import 'package:built_value/serializer.dart';
/// Alternative serializer for [Duration].
///
/// Install this to use ISO8601 compatible format instead of the default
/// (microseconds). Use [SerializersBuilder.add] to install it.
///
/// Note that this serializer is not 100% compatible with the ISO8601 format
/// due to limitations of the [Duration] class, but is designed to produce and
/// consume reasonable strings that match the standard.
class Iso8601DurationSerializer extends PrimitiveSerializer<Duration> {
@override
Duration deserialize(Serializers serializers, Object? serialized,
{FullType specifiedType = FullType.unspecified}) =>
_parseDuration(serialized as String);
@override
Object serialize(Serializers serializers, Duration object,
{FullType specifiedType = FullType.unspecified}) =>
_writeIso8601Duration(object);
@override
Iterable<Type> get types => BuiltList(const [Duration]);
@override
String get wireName => 'Duration';
Duration _parseDuration(String value) {
final match = _parseFormat.firstMatch(value);
if (match == null) {
throw FormatException('Invalid duration format', value);
}
// Iterate through the capture groups to build the unit mappings.
final unitMappings = <String, int>{};
// Start iterating at 1, because match[0] is the full match.
for (var i = 1; i <= match.groupCount; i++) {
final group = match[i];
if (group == null) continue;
// Get all but last character in group.
// The RegExp ensures this must be an int.
final value = int.parse(group.substring(0, group.length - 1));
// Get last character.
final unit = group.substring(group.length - 1);
unitMappings[unit] = value;
}
return Duration(
days: unitMappings[_dayToken] ?? 0,
hours: unitMappings[_hourToken] ?? 0,
minutes: unitMappings[_minuteToken] ?? 0,
seconds: unitMappings[_secondToken] ?? 0,
);
}
String _writeIso8601Duration(Duration duration) {
if (duration == Duration.zero) {
return 'PT0S';
}
final days = duration.inDays;
final hours = (duration - Duration(days: days)).inHours;
final minutes = (duration - Duration(days: days, hours: hours)).inMinutes;
final seconds =
(duration - Duration(days: days, hours: hours, minutes: minutes))
.inSeconds;
final remainder = duration -
Duration(days: days, hours: hours, minutes: minutes, seconds: seconds);
if (remainder != Duration.zero) {
throw ArgumentError.value(duration, 'duration',
'Contains sub-second data which cannot be serialized.');
}
final buffer = StringBuffer(_durationToken)
..write(days == 0 ? '' : '$days$_dayToken');
if (!(hours == 0 && minutes == 0 && seconds == 0)) {
buffer
..write(_timeToken)
..write(hours == 0 ? '' : '$hours$_hourToken')
..write(minutes == 0 ? '' : '$minutes$_minuteToken')
..write(seconds == 0 ? '' : '$seconds$_secondToken');
}
return buffer.toString();
}
// The unit tokens.
static const _durationToken = 'P';
static const _dayToken = 'D';
static const _timeToken = 'T';
static const _hourToken = 'H';
static const _minuteToken = 'M';
static const _secondToken = 'S';
// The parse format for ISO8601 durations.
static final _parseFormat = RegExp(
'^P(?!\$)(0D|[1-9][0-9]*D)?'
'(?:T(?!\$)(0H|[1-9][0-9]*H)?(0M|[1-9][0-9]*M)?(0S|[1-9][0-9]*S)?)?\$',
);
}