Better Know APOC #3 : apoc.date.parse & format
Neo4j.Version | 3.0.0 |
APOC Version | 3.3.0.1 |
If you haven’t already, please have a look at the intro post to this series, it’ll cover stuff which I’m not going to go into on this.
Dates! For some reason people keep on wanting to keep track of dates (and indeed times) in their systems. Neo4j for a long time was leading the charge in rejecting your modern concepts of date and time, unfortunately – many people still want to use them, and much like the ‘paperless office’ a ‘DateTimeless office’ doesn’t really seem like it’ll be on the cards any time soon.
For a lot of people this meant hand rolling code to cope with the progress Neo4j had foisted upon them. I for example, have taken to storing DateTimes in ‘Tick’ form, because nothing says readable like ‘636812928000000000
‘ (that’s Christmas day, 2018 btw). Let’s imagine code wise I want to display that – in C# it’s a doddle (and I assume Java) I just create a new DateTime and output it to the screen, but what if I wanted to do this using Cypher? Or indeed want to add stuff to my database without having to write an application to do that?
Introducing apoc.date.parse
and apoc.date.format
I’m covering both of these as they are complimentary, and typically if you want to use one, you’ll probably want to use the other.
What do they do?
‘parse
‘ parses a given date time string (something like ‘2018/12/25 01:02:03
‘), ‘format
‘ takes a ‘parsed’ value and converts it to a string.
Setup – Neo4j.conf
Nothing! These are safe and require no conf changes to use.
apoc.date.parse
We’ll look at parse first, as typically this is where most people do – gotta get that data in!
Ins and Outs
Tapping apoc.help('apoc.date.parse')
Inputs | (time :: STRING?, unit = ms :: STRING?, format = yyyy-MM-dd HH:mm:ss :: STRING?, timezone = :: STRING?) |
Outputs | (INTEGER?) |
Inputs
There’s always an input, and parse
is no different!
Time
This is our date and time string (badly named), you can pass in just a date:
'2018/3/20'
Using ‘/’, or ‘-‘ separators, it’s all good:
'2018-03-20'
You can add a time:
'2018-03-20 13:34.12'
All you need to do is ensure the pattern you use is reflected in the ‘format’. The default format is listed below.
Unit
This is your output unit, default is millisecond (ms), the values you can convert to are:
- Millisecond (ms/milli/millis/milliseconds)
- Second (s/second/seconds)
- Minutes (m/minute/minutes)
- Hours (h/hour/hours)
- Days (d/day/days)
So you can pass in ‘ms’ or ‘milli’ and get the same output.
Format
Default wise – we’re looking at: yyyy-MM-dd HH:mm:ss
which is:
- Full year – all the digits, ’18’ will be treated as the year ’18’, not 2018!
- Month – 1 or 2 digits, i.e. 1 = January or 01 = January, fancy.
- Day – Again, 1 or 2 digits
- Hours – in 24 hour format, so if you want 1pm, that’s 13,
- Minutes – 1 or 2 digits – I don’t think I need tell you the number of minutes in an hour (right??)
- Seconds – 1 or 2 digits – and again, the traditional number of seconds in a minute.
But wait! That’s not all – do you want to provide your own format? Not interested in time? Only interested in time? Of course! Just put in your own format string (in the Java format) –
- Just Date:
'yyyy-MM-dd'
- Just Time:
'HH:mm:ss'
Just for clarification – the capitalisation of the ‘M’ is important, lowercase = minutes, upper case = months.
Timezone
You’ve got 3 options here, the full name of the timezone, the abbreviation, or something depicting the hours difference:
- Full name:
Europe/London
(I can only assume we’ll need to get this to be renamed to something like ‘Brigreatain/London
‘ or similar – I’ve put the ‘great’ back into Britain) – A full list of these are available on the great wikipedia. - Abbreviation
PST
,UTC
,GMT
obviously these are generally more broad strokes than a specific country. - Custom
GMT +8:00
GMT -8:00
Generally it’s recommended to use the full name. If you choose to not pass in a timezone, the default is ""
, now, you might well ask yourself –
OK does that mean we’re looking at the timezone of my machine? The machine Neo4j is running on? Actually – what does it mean?!
Well – from the code we can see that it’s UTC
, so that’s that cleared up.
Output
Your converted value – or an error (Ha!).
apoc.date.format
We’re gonna jump straight into ‘format’ – as examples wise we may as well put the two together in a date field sandwich.
Ins and Outs
So as to not break with tradition, let’s hit ‘help’: apoc.help('apoc.date.format')
Inputs | (time :: INTEGER?, unit = ms :: STRING?, format = yyyy-MM-dd HH:mm:ss :: STRING?, timezone = :: STRING?) |
Outputs | (STRING?) |
Inputs
Despite looking like I just copy/pasted from above, you’ll note a key difference, ‘time’ is now an integer. Exciting!
Time
This is our date and time in Unit format. By that I mean if you’re wanting to convert from milliseconds to readable, you can do that, or even seconds to readable, all you have to do is set the…
Unit
These are the same units as above, so I won’t go over them again. Default wise we’re looking at ‘ms
‘.
Format
Your output format this time. So even if you stored right down to the millisecond and want to see only the year, you can do that. The default is the same as with Parse – ‘yyyy-MM-dd HH:mm:ss
‘
Timezone
Did you store as UTC but want to see this in PST? Go for your life! As before the default is UTC
.
Output
Your input in a nice readable string format. This is one of those functions we know Michael didn’t write – as he understands ticks natively. Seriously – when you next meet him, ask the time – you’ll get a LONG in response, it is a thing of wonder.
Examples
The obligatory examples section
The Basics (look Mum! No (optional) params!)
We always need to provide at least a date/time to be able to parse, so:
RETURN apoc.date.parse('2018-03-31 13:14:15')
Gets you:
1522502055000
So let’s pass that back into our format function:
WITH apoc.date.parse('2018-03-31 13:14:15') AS inny WITH apoc.date.format(inny) AS outy, inny RETURN *
inny | outy |
1522502055000 | “2018-03-31 13:14:15” |
This is the way we’ll proceed with the examples from here on in..
The I want to only see dates example
WITH apoc.date.parse('2018-03-31', 'ms', 'yyyy-MM-dd') AS inny WITH apoc.date.format(inny) AS outy, inny RETURN *
inny | outy |
1522454400000 | “2018-03-31 00:00:00” |
Note, I parsed only the date, but returned the date and time, this is just to prove that the time isn’t parsed, or rather is but set to 00:00:00.
I’m not going to go into just time, I think we can all work that out.
The Timezone Fun Example
(Loosest sense of the word ‘fun’ here) – we pass in a full date with time, using the default of UTC, then convert is back to a PST time, KERRRRAZY.
WITH apoc.date.parse('2018-03-31 13:14:15') AS inny WITH apoc.date.format(inny, 'ms', 'yyyy-MM-dd HH:mm:ss', 'PST') AS outy, inny RETURN *
inny | outy |
1522502055000 | “2018-03-31 06:14:15” |
Summing up the experience
The DateTime conversion stuff is something that is useful with queries, you can pair it with ‘timestamp()’ if you want to add things like a ‘last logged in’ date:
MATCH (u:User {Id: '123'}) SET u.LastLoggedIn = timestamp(), u.LastLoggedInReadable = apoc.date.format(timestamp())
Storing date and times as integers makes querying for them easy, and apoc.date.parse makes the query readable:
//Find users who logged in this year MATCH (u:User) WHERE u.LastLoggedIn > apoc.date.parse('2018-01-01', 'ms', 'yyyy-MM-dd) RETURN u
I personally think that’s better than:
MATCH (u:User) WHERE u.LastLoggedIn > 1514764800000 RETURN u
Anyhews, enough!