relative static method

String relative(
  1. DateTime dateTime,
  2. {DateTime? relativeTo,
  3. Duration? formatAfter,
  4. String format = AmericanDateTimeFormats.abbrWithComma,
  5. bool round = true,
  6. bool abbr = false,
  7. Map<UnitOfTime, String>? abbreviations,
  8. int levelOfPrecision = 0,
  9. UnitOfTime minUnitOfTime = UnitOfTime.second,
  10. UnitOfTime maxUnitOfTime = UnitOfTime.year,
  11. bool excludeWeeks = false,
  12. String? ifNow,
  13. String? prependIfBefore,
  14. 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;
}