Cypher Sleuthing: Dealing with Dates,
Part 2

Developer Relations Engineer at Neo4j
9 min read

Date Truncation
If you read the Cypher documentation on truncating dates, the wording is accurate but kinda makes my head spin. So let me try to translate it. Cypher manual text: A temporal instant value can be created by truncating another temporal instant value at the nearest preceding point in time at a specified component boundary (namely, a truncation unit). A temporal instant value created in this way will have all components which are less significant than the specified truncation unit set to their default values. My version: You can trim (truncate) a temporal value at a specific point, which sets any smaller units to default values. For instance, truncating the date 2021–04–21 to the year means the year value will be preserved, and the month and day values will be defaulted to 01, returning a result of 2021–01–01. This can be helpful if you want to default a search to generalize all dates by year or month. Note that if you want to completely shorten the date to the desired component (e.g. 2021–04–21 to just 2021), you can do that by accessing the component of the value. This will be covered more in-depth in another post. Let’s look at a few examples of truncating dates. Each of the Neo4j temporal instants (Date, Time, Datetime, LocalDatetime, LocalTime) can use the .truncate() at the end. Just as with the last post, we will stick with the more common Date, Time, and Datetime values. While LocalDatetime and LocalTime are valid and supported, there are very few cases that truly require the use of local, and it is more complex to work with. Example 1: Truncating a datetime at the yearWITH datetime.truncate(‘year’,datetime()) as truncatedDatetime MATCH (b:BlogPost) WHERE b.publishedDatetime >= truncatedDatetime RETURN b.publishedDatetime, truncatedDatetime;
WITH date.truncate(‘month’,date()) as truncatedDate MATCH (p:Project) WHERE p.expectedEndDate > truncatedDate RETURN p.expectedEndDate, truncatedDate;
WITH time.truncate(‘hour’,time(‘09:30:52–06:00’)) as truncatedTime MATCH (p:Presentation) WHERE time.truncate(‘hour’,p.time) = truncatedTime RETURN time.truncate(‘hour’,p.time), truncatedTime;
Cypher Duration
A duration in Cypher is the amount of time between two temporal instants. Whether we are using the duration itself to capture a length of time or calculating distance between points in time, these values are incredibly useful for time measurements. There are 2 different ways to state durations, listed as follows:- Unit-based amounts (literal P and/or T + numeric value + component id) — e.g. P1Y3M10D or {hours: 24}.
- Date and Time instants (literal P and/or T + date and/or time value) — e.g. P20210419 or PT090000.
MERGE (p:Person)-[r:BOOKED]->(v:Vacation) SET v.length = duration(‘P5D’) RETURN v.length as vacationDuration;
MERGE (d:Dose)-[r:PRESCRIBED_AMOUNT]->(m:Medicine) SET d.frequency = duration(‘PT4H’) RETURN d.frequency;
MATCH (:Employee)-[rel:ASSIGNED]-(:Project) RETURN rel.startDate as date, duration(‘P’+ rel.startDate) as duration;
MATCH (d:Dose) MERGE (p:Person)-[r:TAKES]->(d) SET d.dose1Time = duration(‘PT093000’) RETURN d.dose1Time;
Duration Precision
One thing to note is that there are very specific rules that duration follows for calculations. Durations are stored as months, days, seconds, nanoseconds. This is because some components of time can vary — hours in a day (due to daylight savings time), days in a month (28, 29, 30, 31), etc. This is the reason you might notice hour and minute calculations diverting to the more precise and consistent second values. This is why our earlier queries (listed again below) were converted to seconds. In the first query for medicine dose frequency, 4 hours is converted to seconds because larger time values cannot be assumed as 100% consistent at all times. This could actually be a life-and-death situation if dose frequency was not calculated properly. The same scenario exists for the second query. If we took the dose at 9:30AM on a regular day or on a day that changed to/from Daylight Savings Time, this time could be off. It is more precise to calculate the seconds from midnight (34,200 seconds / 60 seconds in minute / 60 minutes in hour = 9.5 hours from midnight). This ensures no incorrect storage values and that users can calculate the time based on their circumstances.MERGE (d:Dose)-[r:PRESCRIBED_AMOUNT]->(m:Medicine) SET d.frequency = duration(‘PT4H’) RETURN d.frequency;
MERGE (p:Person)-[r:TAKES]->(d:Dose) SET d.dose1Time = duration(‘PT093000’) RETURN d.dose1Time;We will see how to convert these small, precise values into more meaningful amounts (like hours) in the next post. For now, we will simply deal with the math of turning durations into precise amounts and finding the difference between 2 date values.
Calculations with Dates and Durations
There are a few ways to go about using durations besides for the plain amounts we saw in the last section — for instance, adding or subtracting temporal amounts and calculating the difference between 2 dates. I would guess that these are probably the most common usages for durations. We will get some examples below of each, starting with adding or subtracting durations from dates. Example 1: Subtract 2.5 months from end date to calculate start dateMATCH (p:Project) SET p.expectedStartDate = p.expectedEndDate — duration(‘P2.5M’) RETURN p.expectedEndDate, p.expectedStartDate;
MATCH (c:Conference) SET c.endDatetime = c.startDatetime + duration({hours: 30}) RETURN c.startDatetime, c.endDatetime;
MATCH (d:Dose) SET d.dose2Time = d.dose1Time + d.frequency RETURN d.dose2Time;
Calculate Difference Between 2 Dates with duration.between
Now let’s calculate the difference between 2 dates. We will need to use duration.between to compare two dates and find the difference. Example 1: Calculate duration between project start and end datesMATCH (:Employee)-[rel:ASSIGNED]-(p:Project) RETURN rel.startDate as assigned, p.expectedEndDate as expectedCompletion, duration.between(rel.startDate, p.expectedEndDate) as lengthAssigned;
MATCH (:Employee)-[rel:ASSIGNED]-(p:Project) RETURN rel.startDate as assigned, duration.between(rel.startDate, date()) as timeSpent;
MATCH (b:BlogPost) RETURN date(‘2021–03–22’) as started, b.publishedDatetime as published, duration.between(date(‘2021–03–22’), b.publishedDatetime) as amountOfTimeSpent;
Wrapping Up!
We saw how to use Cypher’s duration to measure distance in time — whether starting from a length like 2 days, adding or subtracting an amount from a date or time, or finding the difference between 2 dates/times. While some of the amounts returned converted to more precise (and maybe less meaningful) values, we found that this occurs on purpose to ensure consistent amounts that can be relied upon no matter the time of year, geographic location, or the time changes observed. In the next post, we will cover formatting and date components. We will see how to transform these precise values into more meaningful values, as well as how to access components of full date/time values and translate other temporal amounts into different units (i.e. 120 seconds into 2 minutes, or 72 hours into 3 days). Tune in next time and happy coding! To read the next part of this series, read part 3.Resources
- Cypher manual: Durations
- Cypher manual: Create durations from units or from dates and times
- Cypher manual: Compute duration from 2 temporal instants
- Neo4j sandbox: Test out date/time on a free instance
- Blog post: Part 1 of Cypher Sleuthing with Dates
Cypher Sleuthing: Dealing with Dates Part 2 was originally published in Neo4j Developer Blog on Medium, where people are continuing the conversation by highlighting and responding to this story.