Velocitips: Switch email content based on day/time

Updated 8/14/2018: Added day-with-Nth-or-Nst example.
Updated 1/14/2018: Added how-long-will-promo-last example.
Updated 11/30/2017: Added when-is-7-days-from-today example.

With an assist from Velocity, your emails can have time-responsive content. (And I don't just mean Happy ${day_of_week}, ${first_name}!)

  • When the same email is resent with different primary content (e.g. a weekly newsletter) Velocity can customize secondary content based on the day. Like reader PW, you can show a special promo only in the first send of every month.
  • Like Community user GM, you can pre-create a series of different content blocks, in effect creating a drip campaign from just one asset.
  • If a triggered email is sent off-hours, you can notify the recipient that they'll hear from Sales on the next business day.
  • Or lots of other adventures!

Always include this bit

To run all the snippets below, include common variables at the top of the token (or in a global token) like so:

#set( $defaultTimeZone = $date.getTimeZone().getTimeZone("America/New_York") )
#set( $defaultLocale = $date.getLocale() )
#set( $calNow = $date.getCalendar() )
#set( $ret = $calNow.setTimeZone($defaultTimeZone) )
#set( $calConst = $field.in($calNow) )
#set( $ISO8601DateOnly = "yyyy-MM-dd" )
#set( $ISO8601DateTime = "yyyy-MM-dd'T'HH:mm:ss" )
#set( $ISO8601DateTimeWithSpace = "yyyy-MM-dd HH:mm:ss" )
#set( $ISO8601DateTimeWithMillisUTC = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" )

Note on date-like strings

Date magic needs real Java Date and Calendar objects, not Strings that happen to look like dates. No matter if fields originally had a Date/DateTime datatype in another system, even in Marketo itself – if they’ve been stringified on their way into the Velocity context, they need to be “resuscitated” into living objects.

So in a few of the examples below, a Velocity (e.g. Java) String is parsed into a Date, then converted into a Calendar. To do the parsing, you need to know the exact format, so Velocity knows where to locate the years, months, days, and so on.

The $ISO8601WithSpace format ("2019-12-07 13:30:00") is how Marketo DateTime fields appear in Velocity.

I feel like noting that’s a perfectly valid format within the international standard, ISO 8601: whitespace is ignored, and you aren’t required to have a T (or any separator!) between the date and time. Including the T is often presented as “the” ISO 8601 format, but it’s merely “the most common.”[1]

Regardless, the $ISO8601DateTime and $ISO8601DateTimeWithMillisUTC formats, which both have a T separating the date and time ("2019-12-07T13:30:00"), are vastly more common on the web at large. For example, when JSON is generated from JavaScript (and perhaps stored in a Marketo Textarea field) the format is $ISO8601DateTimeWithMillisUTC.

Anyway, knowing what format(s) you’ve got coming in is crucial!

Set your time zone or fail

I cannot stress enough that setting the IANA time zone for your location is critical when using Date or Calendar objects in Velocity. If you do not do this — as is the case, unfortunately, with some old code snippets on Marketo's blog — your code is broken. I won't rehash the reasons why here (happy to in the comments) but believe me you must set the timezone!

Now, for some examples…

Check the current day of the month

#set( $calNow = $date.getCalendar() )
#if( $calNow.get($calConst.DAY_OF_MONTH) <= 7 )
It's one of the first 7 days of the month!
#end

Check the current week

#if( $calNow.get($calConst.WEEK_OF_MONTH) == 1 )
It's the first calendar week of the month!
#end
#if( $calNow.get($calConst.WEEK_OF_MONTH) == $calNow.getActualMaximum($calConst.WEEK_OF_MONTH) )
It's the last week of the month!
#end

Check if it's the Nth Something-day of the month

#if( $calNow.get($calConst.DAY_OF_WEEK) == $calConst.WEDNESDAY && 
     $calNow.get($calConst.DAY_OF_WEEK_IN_MONTH) == 3 )
It's the 3rd Wednesday of the month!
#end

Is it during business hours?

#set( $calStartOfBusiness = $date.getCalendar() )
#set( $ret = $calStartOfBusiness.setTimeZone($defaultTimeZone) )
#set( $ret = $calStartOfBusiness.set(
   $calStartOfBusiness.get($calConst.YEAR),
   $calStartOfBusiness.get($calConst.MONTH),
   $calStartOfBusiness.get($calConst.DAY_OF_MONTH),
   8,
   0,
   0
) )
#set( $ret = $calStartOfBusiness.set($calConst.MILLISECOND,0) )
#set( $calCloseOfBusiness = $date.getCalendar() )
#set( $ret = $calCloseOfBusiness.setTimeZone($defaultTimeZone) )
#set( $ret = $calCloseOfBusiness.set(
   $calCloseOfBusiness.get($calConst.YEAR),
   $calCloseOfBusiness.get($calConst.MONTH),
   $calCloseOfBusiness.get($calConst.DAY_OF_MONTH),
   17,
   0,
   0
) )
#set( $ret = $calCloseOfBusiness.set($calConst.MILLISECOND,0) )
#if( $calNow.compareTo($calStartOfBusiness) >= 0 && $calNow.compareTo($calCloseOfBusiness) <= 0 )
It's during business hours!
#end

Note here that 8,0,0 and 17,0,0 are setting the hour, minute, and second respectively, in 24-hour time.

8,0,0 means 08:00:00 or 8 a.m. and 17,0,0 means 17:00:00 or 5 p.m.

Is it a business day?

#set( $businessDays = [
  $calConst.MONDAY,
  $calConst.TUESDAY,
  $calConst.WEDNESDAY,
  $calConst.THURSDAY,
  $calConst.FRIDAY
] )
#if( $businessDays.contains($calNow.get($calConst.DAY_OF_WEEK)) )
It's a work day!
#end

You can of course combine this business days check with business hours above.

Is our timed promo active?

#set( $calStartOfPromo = $convert.toCalendar(
  $convert.parseDate(
    "2017-11-15T00:00:00", 
    $ISO8601DateTime, 
    $defaultLocale, 
    $defaultTimeZone 
  )
) )
#set( $calEndOfPromo = $convert.toCalendar(
  $convert.parseDate(
    "2017-12-01T00:00:00", 
    $ISO8601DateTime, 
    $defaultLocale, 
    $defaultTimeZone 
  )
) )
#if( $calNow.compareTo($calStartOfPromo) >=0 && $calNow.before($calEndOfPromo) )
The promo is active!
#end

Note again the mandatory use of defaultTimeZone when initializing new Dates.

And check out how I'm using compareTo and before to see if we're currently greater than or equal to the start date and less than the end date.

You could substitute compareTo($calEndOfPromo) < 0 for before($calEndOfPromo) as it's the same logic, just a tiny bit longer.

Promo expires 7 days from today (whenever that is)

$calNow.add($calConst.DATE,7)
#set( $FRIENDLY_24H_DATETIME_WITH_FRIENDLY_TZ = "MMM dd, yyyy HH:mm z" )
Our promo expires 7 days from today, which is
${date.format(
  $FRIENDLY_24H_DATETIME_WITH_FRIENDLY_TZ,
  $calNow,
  $defaultLocale,
  $defaultTimeZone
)}

This quickie shows how to shift a certain number of days from today. To go backwards 7 days, use add($calConst.DATE,-7) (there's no explicit subtract method).

How long will the promo last?

#set( $ret = $calNow.set(
   $calNow.get($calConst.YEAR),
   $calNow.get($calConst.MONTH),
   $calNow.get($calConst.DAY_OF_MONTH),
   0,
   0,
   0
) )
#set( $ret = $calNow.set($calConst.MILLISECOND,0) )
#set( $calEndOfPromo = $convert.toCalendar(
  $convert.parseDate(
    "2018-01-22T00:00:00", 
    $ISO8601DateTime, 
    $defaultLocale, 
    $defaultTimeZone 
  )
) )
#set( $diffRemaining = $date.difference($calNow,$calEndOfPromo) )
#set( $daysRemaining = $convert.toInteger($diffRemaining.getDays()) )
#set( $weeksRemaining = $convert.toInteger($diffRemaining.getWeeks()) )
The promo 
#if( $weeksRemaining > 0 )
ends in ${weeksRemaining} ${display.plural($weeksRemaining,"week","weeks")}!
#elseif( $daysRemaining > 0 )
ends in ${daysRemaining} ${display.plural($daysRemaining,"day","days")}!
#elseif( $daysRemaining == 0 )
ends today!
#else
already ended!
#end

Unlike adding and subtracting days to/from today, differencing dates is relatively complex.

First, you usually[2] want to align dates to midnight (yyyy-MM-ddT00:00:00.000) boundaries. That's why I set the hours, minutes, seconds, and milliseconds of $calNow all to zero, to create a Calendar object representing “Today at midnight.”

Then the end date is also anchored at 00:00:00 (you could use any time of day, as long it's the same for start and end, but midnight is easiest).

Once aligned, you won't get any fractional-day surprises with $date.difference(). difference() returns a robust Comparison object, from which you get the count of days, weeks, and so on between the dates.

I also took this opportunity to use $display.plural(), which isn't a date-related feature but just another cool Velocity thing you should know. plural() works with Integers, though, while Comparison.get*() accessors return Longs. That's why $convert.toInteger() is in there.

Times like this, you can see why I say Velocity is anything but simple. So if you're really a newbie, you shouldn't say, “I'm not too advanced with Velocity” as if you're just a couple scripts away from mastery. To truly add Velocity to your skill stack is a slow grind.

Add an ordinal indicator to the day, like “1st” or “15th” instead of just “1” or “15”

#define( $enUSDayOrdinalIndicators )
1st 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th 16th 17th 18th 19th 20th 21st 22nd 23rd 24th 25th 26th 27th 28th 29th 30th 31st
#end
#set( $indicatorList = $enUSDayOrdinalIndicators.toString().trim().split("\s?\d+") )
Today with a friendly ordinal indicator (is
${date.format(
  "EEEE, MMMM d'${indicatorList[$calNow.get($calConst.DAY_OF_MONTH)]}'",
  $calNow,
  $defaultLocale,
  $defaultTimeZone
)}

This add-on will give you a super-friendly date display, which isn't natively supported by Java or Velocity:

Wednesday, August 15th

(The “th” is the non-native part.)

It should be clear that this is very culturally-specific stuff, and since it isn't part of the localized parts of Java Calendar you would have to add your own localized Nth and Nst for locales other than en-*, some of which never use indicators at all, like Swedish.

Learning More

Get confused enlightened by the Java Calendar docs.


Notes

[1] As in JavaScript’s toISOString() of course, and how Java’s ISO_INSTANT constant just refers to "yyyy-MM-dd'T'HH:mm:ssZ" as “ISO.”

The ancient W3C note on Date and Time Formats defines an ISO 8601 profile whose formats always have the T, but doesn’t conflate the profile with the ISO 8601 standard as a whole.

[2] Exception is when a period ends at, say, exactly 5:00 p.m. on a certain date and you do want to take hours (fractional days) into account. Sweepstakes laws might require such precision. Or if you're trying to do something like hours-until-webinar, you'd also want to align to hours (still zero out minutes/seconds/millis for sanity).