One reason why ISO8601 is the best date string format, is that you can simply append as much precision as you like. Given 2 integers representing seconds and nanoseconds that have passed since 1970-01-01T00:00:00, return an ISO8601 string as described below.


The standard output format without timezone looks like this:


The date is encoded as "year, month,day" with 4,2,2 digits respectively, separated by a "-". The time of day is encoded as "hour, minute, seconds" with 2,2,2 digits respectively. Then, optionally a dot with exactly 6 digits of precision can follow, encoding microseconds that have passed after the given date+(time in hours+minutes+seconds) since. This is only appended if it'd be not equal to 000000. See examples below

Yes, we are allowed to append 6 digits(microseconds) of precision and theoretically more are simply appendable, though not defined further in the standard.


You'll get 2 integers(seconds, nanoseconds). For the sake of simplicity, let's constrain them to be within 0 <= x < 10^9 both. Make sure to discard/round down any precision beyond microseconds.


Input: 616166982 , 34699909     Output: 1989-07-11T13:29:42.034699
Input: 982773555 , 886139278    Output: 2001-02-21T16:39:15.886139
Input: 885454423 , 561869693    Output: 1998-01-22T07:33:43.561869
Input: 0         , 100000       Output: 1970-01-01T00:00:00.000100
Input: 0         , 1000         Output: 1970-01-01T00:00:00.000001
Input: 0         , 999          Output: 1970-01-01T00:00:00
Input: 999999999 , 999999999    Output: 2001-09-09T01:46:39.999999


Provide a function that takes in 2 integers as described in the Input section and returns a String as described in the Output section.

For details and limitations for input/output please refer to the default input/output rules.

This is codegolf: Shortest solution in bytes wins.

  • 4
    \$\begingroup\$ I'd recommend some more test cases just for clarity, and you should probably specify what ISO8601 is; challenges should be complete on their own for the most part. \$\endgroup\$
    – hyper-neutrino
    Commented Oct 19, 2020 at 17:06
  • 1
    \$\begingroup\$ added! Did I miss anything? \$\endgroup\$
    – jaaq
    Commented Oct 19, 2020 at 17:23
  • 1
    \$\begingroup\$ Looks pretty good for now. \$\endgroup\$
    – hyper-neutrino
    Commented Oct 19, 2020 at 17:45
  • \$\begingroup\$ I think you should add an input example where the nanos is 0, since that's potentially a special case (I know it was for me). \$\endgroup\$
    – M. Justin
    Commented Oct 20, 2020 at 7:28
  • 1
    \$\begingroup\$ The question does not state whether this is counting leap seconds. \$\endgroup\$
    – JdeBP
    Commented Oct 20, 2020 at 18:24

14 Answers 14


Bash + sed, 37, 59, 50 bytes

printf '%(%FT%T)T.%06d' $1 $[$2/1000]
printf "%(%FT%T)T.%06d" $1 ${2::-3}|sed s/\\.0*$//

Try it online!

8 bytes saved thanks to @DigitalTrauma

  • 1
    \$\begingroup\$ You're failing the \$6^{th}\$ testcase. Microseconds are only supposed to be appended if not equal to \$0\$. \$\endgroup\$
    – Noodle9
    Commented Oct 19, 2020 at 22:55
  • \$\begingroup\$ @Noodle9, fixed \$\endgroup\$ Commented Oct 20, 2020 at 6:30
  • \$\begingroup\$ @DigitalTrauma, yes ${2::-3} saves 2, the space is not needed after the second : \$\endgroup\$ Commented Oct 20, 2020 at 22:15
  • \$\begingroup\$ Does this work regardless of the system's time zone? \$\endgroup\$
    – M. Justin
    Commented Oct 22, 2020 at 17:35
  • \$\begingroup\$ @M.Justin right, fortunately on tio timezone is utc, otherwise should be TZ=UTC printf ... \$\endgroup\$ Commented Oct 22, 2020 at 17:43

Wolfram Language (Mathematica), 91 bytes


Try it online!

Mathematica supports milliseconds, but not microseconds.


C (gcc), 108 106 bytes

Saved 2 bytes thanks to ceilingcat!!!

o[9];f(s,n)long s;{strftime(o,99,"%FT%T",gmtime(&s));printf((n/=1e3)?"%s.%06d":"%s",o,n);}

Try it online!

Inputs seconds and nanoseconds as integers and outputs the formatted date/time to stdout.


Perl 5 (-p -MPOSIX+strftime -Minteger), 55, 51, 60 bytes

Thanks to @Abigail for giving me the idea to change the input format. + 9 bytes to handle the microseconds=0 case.


Try it online!

  • 2
    \$\begingroup\$ If you add -F/,/ as an option, replace $` by $_, and $' by $F[1], you can save a byte. \$\endgroup\$
    – Abigail
    Commented Oct 19, 2020 at 20:37

Japt, 32 28 bytes

Takes input as a pair of strings, with the nanoseconds first. Can save (at least) 4 bytes if we can include leading 0s with the nanoseconds.

ùT9 ¯6
pU=n g)iÐV*A³ s3 ¯UÄ9

Try it

Or, to "translate" that to JavaScript:

    U.repeat(U=Math.sign(parseInt(U))).replace(/^/,new Date(V*10**3).toISOString().slice(0,U+19))
ùT9 ¯6\npU=n g)iÐV*A³ s3 ¯UÄ9     :Implicit input of strings U=nanoseconds & V=seconds
ù                                 :Left pad U
 T                                :  With 0
  9                               :  To length 9
    ¯6                            :Slice to length 6
      \n                          :Reassign to U
        p                         :Repeat U
         U=                       :  Reassign to U
           n                      :  Convert to integer
             g                    :  Get sign
              )                   :End repeat
               i                  :Prepend
                Ð                 :  Create Date object from
                 V*               :    V multiplied by
                   A              :    10
                    ³             :    Cubed
                      s3          :  To ISO String
                         ¯U       :  Slice to length U
                           Ä9     :    +19

Groovy, 62 60 68 bytes


Try it online!


The Java ofEpochSecond(epochSecond, nanoAdjustment) method returns the Instant corresponding to the given seconds and nanoseconds (exactly what we want for this challenge). The nanoseconds are bitwise-ORed with 1 to ensure we never end up with exactly 0 or 1_000_000 nanoseconds, while still maintaining the same 6-digit rounding behavior.

"${...}" converts the Instant to a GString consisting of the String value of the instant. The toString representation of an Instant is in ISO-8601 format: "2011-12-03T10:15:30.000000001Z". It automatically excludes extra decimal places past 0/3/6 decimal places (seconds/milliseconds/microseconds) if the remaining digits are 0, hence the earlier bitwise-OR.

"${value}"[0..25] returns characters 0 through 25 of the string, which is up through the sixth decimal place.

-~/\.0+$/ subtracts the first instance of the regex pattern \.0+$ from the resulting string; namely, a decimal place followed by all zeros, followed by the end of the string. This will only match if the string ends in .000000, and the code is one character shorter than subtracting '.000000'.

  • \$\begingroup\$ This looks quite neat. Care to explain a bit, though? \$\endgroup\$
    – Adám
    Commented Oct 20, 2020 at 6:54
  • 1
    \$\begingroup\$ @Adám Added explanation (and found a bug) \$\endgroup\$
    – M. Justin
    Commented Oct 20, 2020 at 7:32
  • \$\begingroup\$ Thanks. So your answer is currently invalid. Maybe delete it, then edit it, and then undelete when ready? This could save you from downvotes. \$\endgroup\$
    – Adám
    Commented Oct 20, 2020 at 8:20
  • \$\begingroup\$ @Adám Fixed the bug with a bitwise OR hack. Reduced by a byte by subtracting a regex rather than a string literal. \$\endgroup\$
    – M. Justin
    Commented Oct 20, 2020 at 8:31

Groovy, 63 bytes

f={s,n->sprintf('%tFT%1$tT.%06d',s*1000L,n/1E3as int)-~/\.0+$/}

Try it online!

A different Groovy approach using format strings (inspired by other similar answers).


APL (Dyalog Unicode) 18.0, 53 bytes (SBCS)

Full program. Prompts for nanoseconds, then seconds.

(¯7×0=f)↓⊃'%ISO%.ffffff'(1200⌶)20 1⎕DT⎕+1E¯6×f←⌊⎕÷1E3

Try it online! (polyfills for and ⎕DT because TIO still uses 17.1)

⎕÷1E3 divide input nanoseconds by 1000

f← assign to f

1E¯6× multiply by 0.000001

⎕+ add input seconds to that

20 1⎕DT convert from UNIX time (seconds since 1970) to Dyalog Date Number (days since 1989-12-31)

'%ISO%.ffffff'(1200⌶) format according to ISO with six-digit fractional second precision

 disclose (because a "string" is an enclosed character vector)

()↓ drop the following number of characters

0=f one if f is zero

¯7× seven from the rear if so (lit. negative seven multiplied by that)


JavaScript (V8), 83 94 ... 78 bytes

s=>n=>new Date(s*1e3).toJSON(n=0|n/1e3).slice(0,20-!n)+`${n+1e6}`.slice(n?1:7)

Try it online!

  • Thanks to @Neil for the great help!
  • Saved another 5 thanks to @Arnauld
  • \$\begingroup\$ You don't need the ()s around new Date(s*1e3), and you can save another 2 bytes using (n/1e6).toFixed(6).slice(2). \$\endgroup\$
    – Neil
    Commented Oct 19, 2020 at 23:20
  • \$\begingroup\$ Also, you're not supposed to have the .000000 when n=0 for some reason. \$\endgroup\$
    – Neil
    Commented Oct 19, 2020 at 23:25
  • \$\begingroup\$ Oh, speaking of misreading the question, I hadn't noticed that you need to truncate the nanoseconds. Never mind. \$\endgroup\$
    – Neil
    Commented Oct 19, 2020 at 23:28
  • \$\begingroup\$ @Neil thanks , I missed that, so I just have to fix the 0 microseconds case and leave the () ? \$\endgroup\$
    Commented Oct 20, 2020 at 0:02
  • \$\begingroup\$ You can save a byte by moving the n=0|n/1e3 into the toISOString parameter, leaving just !n, and another byte by using n?2:8 instead of 6*!n+2 (also note that you only need to add 1e6 and slice by n?1:7). \$\endgroup\$
    – Neil
    Commented Oct 20, 2020 at 9:30

Raku, 46 bytes 40 chars, 42 bytes

{(~DateTime.new($^a+$^b divⅯ/1e6)).chop}

Try it online!

Saved a few bytes by using a non-digit numeral (which could be replaced by others like , but others like 𐄢 or 𑁥 add an extra byte), which allowed the space removed after div. Only trick here was needing to force the round down, and stringify the DateTime to be able to .chop. There may in fact be a bug here in that Rakudo's implementation rounds up, and if we consider it as such, then it can be further golfed to


Which would only be 35 chars/bytes.


Python 3.8 (pre-release) 106, 81 bytes

lambda a,b:datetime.fromtimestamp(a+b//1e3/1e6).isoformat()
from datetime import*

Try it online!

  • \$\begingroup\$ I'll make a start with an example, though there probably still is some room for improvements. \$\endgroup\$
    – jaaq
    Commented Oct 19, 2020 at 17:03
  • 1
    \$\begingroup\$ 81 bytes (the f= doesn't count): Try it online! \$\endgroup\$
    – pxeger
    Commented Oct 21, 2020 at 19:07
  • \$\begingroup\$ Nice! If that's the case I'll add it to the header :) \$\endgroup\$
    – jaaq
    Commented Oct 22, 2020 at 8:08

Excel, 102 101 bytes


Input is seconds in A1 and nanoseconds in B1.

There are two major pieces to this:

25569 is the numerical equivalent of 1970-01-01 in Excel which measures from 1900-01-00 as zero.
A1/86400 converts seconds into days.
"yyyy-mm-ddThh:mm:ss" formats the result, giving us the majority of the desired output.

TEXT(B1/10^9,"."&REPT(0,9)) converts from an integer of nanoseconds to a decimal of seconds.
LEFT(TEXT(~),7) gives the decimal point with the leading 6 digits.
SUBSTITUTE(LEFT(~),".000000",) accounts for a <1,000 nanoseconds by dropping the result.

Aside: Given how friendly Excel is with dates, it makes sense - but is still annoying - that it takes more bytes to deal with .000000 than it does to deal with 1970-01-01T00:00:00. I found alternate approaches with less bytes but they only work if we can round the nanoseconds or display zero values. Alack and alas.

  • 1
    \$\begingroup\$ Given the date constraint of between 0 & 10^9, you can save a byte by changing the year format to "yyy-mm-ddThh:mm:ss" \$\endgroup\$
    – M. Justin
    Commented Oct 23, 2020 at 17:16

J-uby, 42 bytes

Uses -rtime flag to get Time#iso8601.


Attempt This Online!


~(~(:at & Time) & :nsec) |  # Time.at(secs, nsecs, :nsec), then
~:iso8601 & 6 |             # ISO-8601 w/ 6 fractional digits, then
~:[] & 26 & 0               # Get first 26 chars (drop time zone)

Scala 3, 139 bytes

Golfed version. Attempt This Online!

(s,n)=>{val m=Instant.ofEpochSecond(s,n|1).toString;val r=if(m.contains("."))m.replaceAll("\\.0+$","")else m;r.substring(0,r.size.min(26))}

Ungolfed version. Attempt This Online!

import java.time.Instant

object Main {
  def main(args: Array[String]): Unit = {
    def f(s: Long, n: Long): String = {
      val instant = Instant.ofEpochSecond(s, n | 1)  // Ensure nanoseconds are non-zero by OR-ing with 1
      val formattedInstant = instant.toString
      val result = if (formattedInstant.contains(".")) formattedInstant.replaceAll("\\.0+$", "") else formattedInstant
      result.substring(0, result.length.min(26))  // Ensure not to exceed string length

    assert("1989-07-11T13:29:42.034699" == f(616166982, 34699909))
    assert("2001-02-21T16:39:15.886139" ==  f(982773555, 886139278))
    assert("1998-01-22T07:33:43.561869" == f(885454423, 561869693))
    assert("1970-01-01T00:00:00.000100" ==  f(0, 100000))
    assert("1970-01-01T00:00:00.000001" ==  f(0, 1000))
    assert("1970-01-01T00:00:00.000000" ==  f(0, 999))
    assert("2001-09-09T01:46:39.999999"  ==  f(999999999, 999999999))
    assert("1970-01-01T00:00:00.000000" ==  f(0, 0))
    assert("1970-01-01T00:00:00.001000" ==  f(0, 1000000))

    println("All assertions passed.")

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.