Date format: dynamic string depending on how long ago

You probably have seen applications that format dates in a verbose way:

  • “Just now” when it’s been less than a minute
  • Only the time (xx:xx) if it was today and longer ago than a minute
  • “Yesterday, xx:xx” if it was yesterday
  • The weekday and the time if it was within the last couple of days
  • The exact date and time if it was longer ago

This type of format can be found in popular chat apps for example in the chat overview. Let’s implement a date formatter that puts out a string in the above mentioned way.

We start by implementing a new class called DateFormatter with a single public method getVerboseDateTimeRepresentation. We let it expect a UTC DateTime as its purpose is to receive a DateTime and return a string.

class DateFormatter {
  String getVerboseDateTimeRepresentation(DateTime dateTime) {
  }
}

Just now

The first case we deal with is returning “Just now” if the given DateTime is less than a minute old.

DateTime now = DateTime.now();
DateTime justNow = DateTime.now().subtract(Duration(minutes: 1));
DateTime localDateTime = dateTime.toLocal();

if (!localDateTime.difference(justNow).isNegative) {
  return 'Just now';
}

It’s important to make the comparison with the local DateTime as it ensures that it works across every timezone. Otherwise, the result of the difference would always be affected by the difference of the local timezone to the UTC timezone.

Today

Next step is to show the rough time (meaning that it omits the seconds) whenever the given DateTime is less than a day old.

String roughTimeString = DateFormat('jm').format(dateTime);
if (localDateTime.day == now.day && localDateTime.month == now.month && localDateTime.year == now.year) {
  return roughTimeString;
}

You might wonder why 'jm' is used as the positional newPattern argument for the DateTime constructor. That’s because it adapts to the circumstances of the current locale. If we were to use DateFormat('HH:mm'), we would always have 24 hour time format whereas DateFormat('jm') would use 12 hour time and add am / pm markers if needed. For more information about the difference of skeletons and explicit patterns have a look at the docs.

We could also use the ICU name instead of the skeleton. In this case DateFormat('WEEKDAY') would work as well and is certainly better readable.

Yesterday

Now we want the DateFormatter to prepend Yesterday, if the given DateTime holds a value that represents the day before today.

DateTime yesterday = now.subtract(Duration(days: 1));

if (localDateTime.day == yesterday.day && localDateTime.month == yesterday.month && localDateTime.year == yesterday.year) {
  return 'Yesterday, ' + roughTimeString;
}

We check whether day, month and year of the current DateTime subtracted by a day equal the respective values of the given DateTime and return Yesterday, followed by the rough time string we stored above if the condition evaluates to true.

Last couple of days

Let’s deal with everything less old than 4 days and return the weekday in the system’s language followed by the hours and minutes.

if (now.difference(localDateTime).inDays < 4) {
  String weekday = DateFormat('EEEE').format(localDateTime);

  return '$weekday, $roughTimeString';
}

We compare the current DateTime with the given DateTime and check whether the difference is less than 4 whole days. If so, we use the skeleton EEEE that represents the verbose weekday. Because we don’t provide the second optional argument, it takes en_US as the locale and returns the weekday in that language.

Again, we could also use the ICU name instead of the skeleton so DateFormat('HOUR_MINUTE') would work as well.

Otherwise, return year, month and day

Now if none of the above conditions match, we want to display the date and the time.

return '${DateFormat('yMd').format(dateTime)}, $roughTimeString';

So now we have achieved what we wanted to: depending on how long ago the given DateTime was, we want to return different strings.

Localization of DateFormatter

One thing that this formatter is still lacking is localization. If we used this on a device whose system language is not English, we would still be faced with English expressions.
In order to fix that, we need the current system’s locale. That’s not enough, though, as we also want the phrases "Just now" and "Yesterday" to be translated. That’s why we need localization in general and take the locale from the delegate. Have a look at the i18n tutorial for information on how to set that up.

en.json

{
  "dateFormatter_just_now": "Just now",
  "dateFormatter_yesterday": "Yesterday"
}

de.json

{
  "dateFormatter_just_now": "Gerade eben",
  "dateFormatter_yesterday": "Gestern"
}
import 'package:flutterclutter/app_localizations.dart';
import 'package:intl/intl.dart';

class DateFormatter {
  DateFormatter(this.localizations);

  AppLocalizations localizations;

  String getVerboseDateTimeRepresentation(DateTime dateTime) {
    DateTime now = DateTime.now();
    DateTime justNow = now.subtract(Duration(minutes: 1));
    DateTime localDateTime = dateTime.toLocal();

    if (!localDateTime.difference(justNow).isNegative) {
      return localizations.translate('dateFormatter_just_now');
    }

    String roughTimeString = DateFormat('jm').format(dateTime);

    if (localDateTime.day == now.day && localDateTime.month == now.month && localDateTime.year == now.year) {
      return roughTimeString;
    }

    DateTime yesterday = now.subtract(Duration(days: 1));

    if (localDateTime.day == yesterday.day && localDateTime.month == now.month && localDateTime.year == now.year) {
      return localizations.translate('dateFormatter_yesterday');
    }

    if (now.difference(localDateTime).inDays < 4) {
      String weekday = DateFormat('EEEE', localizations.locale.toLanguageTag()).format(localDateTime);

      return '$weekday, $roughTimeString';
    }

    return '${DateFormat('yMd', localizations.locale.toLanguageTag()).format(dateTime)}, $roughTimeString';
  }
}

We add AppLocalization as the only argument to the constructor of the DateFormatter. Every occasion of a string that contains a language-specific phrase is now altered by the usage of AppLocalization or its locale property.

Now, in order to see our brand new formatter in action, we create a widget that lists hardcoded chats and displays the date of the last sent message in the top right corner.

import 'package:flutter/material.dart';
import 'date_formatter.dart';
import 'app_localizations.dart';

class Chat {
  Chat({
    @required this.sender,
    @required this.text,
    @required this.lastMessageSentAt
  });

  String sender;
  String text;
  DateTime lastMessageSentAt;
}

class MessageBubble extends StatelessWidget{
  MessageBubble({
    this.message
  });

  final Chat message;

  @override
  Widget build(BuildContext context) {
    return Padding(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Row(
            mainAxisAlignment: MainAxisAlignment.end,
            children: [
              Text(
                DateFormatter(AppLocalizations.of(context)).getVerboseDateTimeRepresentation(message.lastMessageSentAt),
                textAlign: TextAlign.end,
                style: TextStyle(
                  color: Colors.grey
                )
              )
            ]
          ),
          Padding(
            padding: EdgeInsets.only(bottom: 16),
            child: Text(
              message.sender,
              style: TextStyle(
                fontSize: 18,
                fontWeight: FontWeight.bold
              )
            ),
          ),
          Text(
            message.text,
          ),
        ],
      ),
      padding: EdgeInsets.all(16)
    );
  }
}

class DateTimeList extends StatelessWidget {
  final List<Chat> messages = [
    Chat(
      sender: 'Sam',
      text: 'Sorry man, I was busy!',
      lastMessageSentAt: DateTime.now().subtract(Duration(seconds: 27))
    ),
    Chat(
      sender: 'Neil',
      text: 'Hey! Are you there?',
      lastMessageSentAt: DateTime.now().subtract(Duration(days: 3))
    ),
    Chat(
      sender: 'Patrick',
      text: 'Hey man, what\'s up?',
      lastMessageSentAt: DateTime.now().subtract(Duration(days: 7))
    ),
  ];

  @override
  Widget build(BuildContext context) {
    return ListView.separated(
      itemBuilder: (BuildContext context, int index) {
        return MessageBubble(message: messages[index]);
      },
      separatorBuilder: (BuildContext context, int index) => Divider(),
      itemCount: messages.length
    );
  }
}

Result

Here is a comparison between the above implemented list with:

  • Chats with dates being displayed without our DateFormatter
  • Chats with dates being displayed with our DateFormatter and English locale
  • Chats with dates being displayed with our DateFormatter and German locale
Absolute date formatting
Date format without using our DateFormater
Verbose date formatting in English
Date format using our DateFormatter

Verbose date formatting in German
Date format using our Dateformatter with locale de

Summary

We have implemented a class that returns a verbose representation of the DateTime object that is provided and adaptively changes depending on how far the date reaches into the past. We have also made this class handle different locales using localization.
This formatter can be useful in several contexts where a date is supposed to be displayed relative to the current date.

If you like what you’ve read, feel free to support me:

🥗Buy me a salad

2 thoughts on “Date format: dynamic string depending on how long ago”

  1. You may consider to refactor those conditions to have unified structure, eg.:

    `now.difference(localDateTime).inDays < 4` – I found this one to be most readable, then first condition may be changed from:

    `!localDateTime.difference(justNow).isNegative` to: `now.difference(localDateTime).inMinutes <= 1`

    same principle applies to yesterday and rough date cases.

    Reply

Leave a Comment