relative static method
- DateTime dateTime,
- {DateTime? relativeTo,
- Duration? formatAfter,
- String format = AmericanDateTimeFormats.abbrWithComma,
- bool round = true,
- bool abbr = false,
- Map<
UnitOfTime, String> ? abbreviations, - int levelOfPrecision = 0,
- UnitOfTime minUnitOfTime = UnitOfTime.second,
- UnitOfTime maxUnitOfTime = UnitOfTime.year,
- bool excludeWeeks = false,
- String? ifNow,
- String? prependIfBefore,
- String? appendIfAfter}
Formats dateTime
to a human-readable relative time format.
relativeTo
defaults to DateTime.now()
.
If formatBefore
is not null
, dateTime
will be formatted to
the format specified by format
if dateTime
occured before
formatBefore
.
If round
is true
, units of time will be rounded up to the
minimum allowed unit of time, as defined by levelOfPrecision
and minUnitOfTime
, see below. If false
, values below the
minimum allowed unit of time will be truncated.
If abbr
is true
, the labels for the units of time (seconds
,
minutes
, hours
, etc...) will be abbreviated to the first character
of each label, respectively. If false
, the entire word will be returned.
A map of abbreviations
can be provided to supply custom abbreviations
to use for any unit of time. The abbreviations will only be applied if
abbr
is true
.
levelOfPrecision
defines the minimum allowable degree of separation from
the maximum unit of time counted. I.e. minutes are 1
degree removed from
hours, and seconds are 2
degrees removed from hours but only 1
degree
removed from minutes. Note: Weeks will not be counted as a unit of
time if excludeWeeks
is true
, see below.
minUnitOfTime
is the minimum unit of time that will be included in
the count. minUnitOfTime.index
must be >= maxUnitOfTime.index
.
maxUnitOfTime
is the maximum unit of time that will be included in
the count.
If excludeWeeks
is true
, weeks won't be counted. Instead, days
will counted up to their respective month's number of days.
If ifNow
is supplied, the value of ifNow
will be returned in the
event the difference is less than the smallest allowed interval of time,
otherwise an empty string will be returned.
If prependIfBefore
is not null
and dateTime
occurs before
relativeTo
, its value will be prepended to the returned string.
If appendIfAfter
is not null
and dateTime
occurs after
relativeTo
, its value will be appended to the returned string.
Implementation
static String relative(
DateTime dateTime, {
DateTime? relativeTo,
Duration? formatAfter,
String format = AmericanDateTimeFormats.abbrWithComma,
bool round = true,
bool abbr = false,
Map<UnitOfTime, String>? abbreviations,
int levelOfPrecision = 0,
UnitOfTime minUnitOfTime = UnitOfTime.second,
UnitOfTime maxUnitOfTime = UnitOfTime.year,
bool excludeWeeks = false,
String? ifNow,
String? prependIfBefore,
String? appendIfAfter,
}) {
assert(minUnitOfTime.index >= maxUnitOfTime.index);
relativeTo ??= DateTime.now();
var difference = dateTime.difference(relativeTo).abs();
if (formatAfter != null && difference >= formatAfter) {
return DateTimeFormat.format(dateTime, format: format);
}
final inverse = relativeTo.isBefore(dateTime);
var startFrom = inverse ? relativeTo : dateTime;
if (difference < _minUnitOfTime(minUnitOfTime, startFrom, inverse)) {
return ifNow ?? '';
}
int count(
Duration duration, [
Duration Function(DateTime, bool)? setDuration,
]) {
var count = 0;
while (difference >= duration) {
count++;
difference -= duration;
startFrom = startFrom.add(duration);
if (setDuration != null) duration = setDuration(startFrom, inverse);
}
return count;
}
void reduce(Duration duration) {
if (duration.inMicroseconds > 0) {
difference -= duration;
}
}
final unitsOfTime = <UnitOfTime, int>{};
for (var unitOfTime in UnitOfTime.values) {
var units = 0;
if (maxUnitOfTime.index <= unitOfTime.index) {
switch (unitOfTime) {
case UnitOfTime.year:
units = count(_lengthOfYear(startFrom, inverse), _lengthOfYear);
break;
case UnitOfTime.month:
units = count(_lengthOfMonth(startFrom, inverse), _lengthOfMonth);
break;
case UnitOfTime.week:
units = excludeWeeks ? 0 : count(Duration(days: 7));
break;
case UnitOfTime.day:
units = difference.inDays;
reduce(Duration(days: units));
break;
case UnitOfTime.hour:
units = difference.inHours;
reduce(Duration(hours: units));
break;
case UnitOfTime.minute:
units = difference.inMinutes;
reduce(Duration(minutes: units));
break;
case UnitOfTime.second:
units = difference.inSeconds;
reduce(Duration(seconds: units));
break;
case UnitOfTime.millisecond:
units = difference.inMilliseconds;
reduce(Duration(milliseconds: units));
break;
case UnitOfTime.microsecond:
units = difference.inMicroseconds;
break;
}
}
unitsOfTime.addAll({unitOfTime: units});
}
final maxUnitOfTimeIndex =
unitsOfTime.values.toList().indexWhere((count) => count > 0);
var minUnitOfTimeIndex = maxUnitOfTimeIndex + levelOfPrecision;
if (levelOfPrecision > 0) {
if (excludeWeeks && minUnitOfTimeIndex >= UnitOfTime.week.index) {
minUnitOfTimeIndex++;
}
minUnitOfTimeIndex = minUnitOfTimeIndex.clamp(0, minUnitOfTime.index);
}
// Increase the value assocaited with [unit] in [unitsOfTime] by `1`.
void increaseUnitOfTime(UnitOfTime unit) {
if (unitsOfTime.containsKey(unit)) {
unitsOfTime[unit] = (unitsOfTime[unit] ?? 0) + 1;
} else {
unitsOfTime.addAll({unit: 1});
}
}
for (var unitOfTime in unitsOfTime.keys.toList().reversed) {
final lastUnit = unitOfTime.index <= minUnitOfTimeIndex;
if (round && maxUnitOfTime.index <= unitOfTime.index - 1) {
final units = unitsOfTime[unitOfTime];
switch (unitOfTime) {
case UnitOfTime.year:
// Years can't be rounded.
break;
case UnitOfTime.month:
if (units! >= 12 || (!lastUnit && units >= 6)) {
unitsOfTime[UnitOfTime.month] = 0;
increaseUnitOfTime(UnitOfTime.year);
}
break;
case UnitOfTime.week:
if (units! >= 4 || (!lastUnit && units >= 2)) {
unitsOfTime[UnitOfTime.week] = 0;
increaseUnitOfTime(UnitOfTime.month);
}
break;
case UnitOfTime.day:
if (excludeWeeks) {
var month = (inverse ? dateTime.month : relativeTo.month) - 1;
var year = inverse ? dateTime.year : relativeTo.year;
if (month == 0) {
month = 12;
year--;
}
final daysInMonth = _daysInMonth(month, year);
if (units! >= daysInMonth ||
(!lastUnit && units > daysInMonth / 2)) {
unitsOfTime[UnitOfTime.day] = 0;
increaseUnitOfTime(UnitOfTime.month);
}
} else {
if (units! >= 7 || (!lastUnit && units >= 4)) {
unitsOfTime[UnitOfTime.day] = 0;
increaseUnitOfTime(UnitOfTime.week);
}
}
break;
case UnitOfTime.hour:
if (units! >= 24 || (!lastUnit && units >= 12)) {
unitsOfTime[UnitOfTime.hour] = 0;
increaseUnitOfTime(UnitOfTime.day);
}
break;
case UnitOfTime.minute:
if (units! >= 60 || (!lastUnit && units >= 30)) {
unitsOfTime[UnitOfTime.minute] = 0;
increaseUnitOfTime(UnitOfTime.hour);
}
break;
case UnitOfTime.second:
if (units! >= 60 || (!lastUnit && units >= 30)) {
unitsOfTime[UnitOfTime.second] = 0;
increaseUnitOfTime(UnitOfTime.minute);
}
break;
case UnitOfTime.millisecond:
if (units! >= 1000 || (!lastUnit && units >= 500)) {
unitsOfTime[UnitOfTime.millisecond] = 0;
increaseUnitOfTime(UnitOfTime.second);
}
break;
case UnitOfTime.microsecond:
if (units! >= 1000 || (!lastUnit && units >= 500)) {
unitsOfTime[UnitOfTime.microsecond] = 0;
increaseUnitOfTime(UnitOfTime.millisecond);
}
break;
}
}
if (lastUnit) break;
unitsOfTime.remove(unitOfTime);
}
unitsOfTime.removeWhere((key, value) => value == 0);
var formattedString = _formatUnits(unitsOfTime, abbr, abbreviations);
if (prependIfBefore != null && dateTime.isAfter(relativeTo)) {
formattedString = '$prependIfBefore $formattedString';
}
if (appendIfAfter != null && dateTime.isBefore(relativeTo)) {
formattedString = '$formattedString $appendIfAfter';
}
return formattedString;
}