8

Scenario:

I am fetching a date value from a file into a variable and it is in DD-MM-YYYY format by default. I have to substract this date from system date. Subtraction is giving incorrect result if I had both dates in DD-MM-YYYY format. So I read a bit on google and decided to format both dates as YYYY-MM-DD as this will give correct value after subtraction. I have System date formatted successfully in YYYY-MM-DD, but facing a hard time to convert the date obtained from the file to YYYY-MM-DD format.

Below solution works fine with single digit dates:

$ date -d $(sed "s/-/\//g" <<< '9-2-1832') +%Y-%m-%d
Output : 1832-09-02

but when I try to convert date in double digits like below:

$ date -d $(sed "s/-/\//g" <<< '19-07-2021') +%Y-%m-%d

I get output

Invalid date '19/07/2021'

Where:

19 - is Date of a month
07 - is Month i.e. July in this case.
2021 - is Year

Desired output as -> 2021-07-19

I am working on RH Linux with Date version as: date (GNU coreutils) 8.22

Please help to provide a solution for above problem.

9
  • 4
    The first one didn't work fine as it gave you 1832-09-02 instead of 1832-02-09 for 9-2-1832. IOW, your date interpreted the date as if it was in US format (m-d-y) instead of d-m-y). Jul 21, 2021 at 11:31
  • @StéphaneChazelas I can reproduce this on my Arch, and using LC_ALL=el_GR.utf8 or LC_ALL=es_ES.utf8 or LC_ALL=fr_FR.utf8 doesn't make a difference, although all of those use DD-MM-YYYY and print the right format with +%x. This is a bug in GNU date, right?
    – terdon
    Jul 21, 2021 at 11:59
  • 2
    @terdon, no, it works as documented, GNU date expects date is a limited number of formats, generally US style, and that's independant of the locale. See info date cal Jul 21, 2021 at 12:18
  • I find GNU date weird: it jumps through hoops to try to accept all sorts of "intuitive" date inputs, but doesn't let you tell it what the format is when it's impossible to guess. (BSD date is much nicer in this respect.) Granged, 19 can't be a month, but GNU date doesn't seem to go so far as to assume it must be a day and adjust accordingly.
    – chepner
    Jul 21, 2021 at 19:30
  • 1
    Do you have no control over the file?  If you could arrange for that to be in YYYY-MM-DD format too, then your life would be much easier :-)  (I don't understand why everyone isn't using ISO-format dates everywhere by now…)
    – gidds
    Jul 22, 2021 at 16:38

10 Answers 10

13

In your case, awk might be a better method:

$ awk -F'-' '{printf("%04d-%02d-%02d\n",$3,$2,$1)}' <<< '19-07-2021'
2021-07-19
4
  • Thanks @Stephane. I have posted my answer which helped solving my problem, but I think yours would work too as the approach looks similar.
    – MayD
    Jul 22, 2021 at 5:29
  • Awk is able to do it directly:awk -F'-' '{print strftime("%Y-%m-%d",mktime($3" "$2" "$1" 0 0 0"))}' <<< '19-07-2021'
    – user232326
    Jul 23, 2021 at 3:59
  • @ImHere True, but it is somewhat longer the simple printf approach, don't you think? ;)
    – AdminBee
    Jul 23, 2021 at 7:09
  • The simple printf doesn't validate the date.
    – user232326
    Jul 23, 2021 at 10:25
13

You could switch to busybox date which allows specifying the input format:

$ date=19-7-2021
$ busybox date -D %d-%m-%Y -d "$date" +%F
2021-07-19

(beware it won't accept 019-007-2021 dates for instance).

Same with the ast-open implementation of date (unlikely to be available out of the box on your RedHat system):

$ date -p %d-%m-%Y -d "$date" +%F
2021-07-19

If you were on BSD instead of a GNU system, you'd do:

$ date -jf %d-%m-%Y -- "$date" +%F
2021-07-19

Or you could switch to zsh instead of bash as is has a date parsing and formatting builtin:

$ zmodload zsh/datetime
$ strftime -rs t %d-%m-%Y $date && strftime %F $t
2021-07-19

(strftime being the builtin that gives access to the standard strftime() and strptime() (with -r) APIs).

It also has proper splitting operators and a printf that can take arguments in arbitrary orders (using the %n$... syntax as found in most printf() implementations including GNU printf(), but not the printf builtin of bash nor the GNU printf standalone utility), and it doesn't treat numbers with leading zeros as octal by default:

$ printf '%3$04d-%2$02d-%1$01d\n' ${(s[-])date}
2021-07-19

Or using an anonymous function:

$ (){ printf '%04d-%02d-%02d\n' $3 $2 $1; } ${(s[-])date}
2021-07-19

Or here, the array reversing operator:

$ printf '%04d-%02d-%02d\n' ${(s[-]Oa)date}
2021-07-19
8

If the format is hardcoded as DD-YY-YYYY, you can also hardcode the whole expression and use substring extraction to reformat, which is very fast:

#!/bin/bash
dmy=$(date +%d-%m-%Y)
ymd="${dmy: -4}-${dmy:3:2}-${dmy:0:2}"
echo "$dmy -> $ymd"

If the format is D[D]-M[M]-YYYY you could use regular expressions to reformat, which is somewhat slower:

#!/bin/bash
dmy="9-2-1932"
if [[ "$dmy" =~ ^([[:digit:]]+)-([[:digit:]]+)-([[:digit:]]+)$ ]] ; then
  ymd="${BASH_REMATCH[3]}-${BASH_REMATCH[2]}-${BASH_REMATCH[1]}"
fi
echo "$dmy -> $ymd"

You can format that a bit nicer with printf:

dmy="9-02-2032"
if [[ "$dmy" =~ ^([[:digit:]]+)-([[:digit:]]+)-([[:digit:]]+)$ ]] ; then
  printf -v ymd '%.4d-%.2d-%.2d' \
    "${BASH_REMATCH[3]}" "${BASH_REMATCH[2]}" "${BASH_REMATCH[1]}"
fi
echo "$dmy -> $ymd"

A former version of this answer is evidence that I didn't read it fully, converting YYYY-MM-DD to DD-MM-YYYY:

#!/bin/bash

ymd=$(date  +%Y-%m-%d)
dmy="${ymd: -2}-${ymd:5:2}-${ymd:0:4}"
echo "$ymd -> $dmy"
3
  • ${ymd: -2}${ymd:4:4}${ymd:0:4}
    – nezabudka
    Jul 22, 2021 at 6:49
  • 1
    Note that one of their examples is the date string 9-2-1832.
    – Kusalananda
    Jul 22, 2021 at 17:09
  • @Kusalananda That was a it hidden, but I can't read anyway. Thanks for the note. Jul 23, 2021 at 17:58
5

Thanks all you guys for pitching in with various solutions. I believe few of the suggested solutions would work out for me as I correlate them to the approach I used in solving it on my own yesterday, right after posting this question.

I used IFS to achieve it.

IFS=- read d m y <<<'19-7-2021'
printf '%.4d-%.2d-%.2d\n' "${y#0}" "${m#0}" "${d#0}"

This would output 2021-07-19.

The ${m#0} etc. removes an initial 0 from the value of $m, in case the original month happens to be 08 or 09 which would otherwise be interpreted as invalid octal numbers.

1
  • 1
    Just FYI you can force bash computations to be in base 10 by using $((10#$m)). It usually helps also because it will not replace single-digit 0 with nothing (as does ${m#0}). Also, even empty variables become 0. But neither can happen in your case, so this suggestion only make the whole slightly more verbose.
    – MoonCactus
    Jul 22, 2021 at 16:59
2

A quick and lean solution that uses only sed:

echo 19-07-2021 | sed -E 's/^([0-9]{1,2})-([0-9]{1,2})-([0-9]{4})$/\3-\2-\1/'
2
  • 5
    They also have an example saying 9-2-1832...
    – Kusalananda
    Jul 22, 2021 at 17:07
  • 1
    @Kusalananda Thanks, fixed. Aug 26, 2021 at 15:33
1
$ perl -lne 'print reverse split /(-)/, s/\b\d\b/0$&/rg' <<<"19-7-2021"
2021-07-19

Split on dash /(-)/ , with the dashes included in the split up array but before that change the date to double digits in day and/or month.

0

I had the same problem in many .txt files and wrote a simple one line:

sed -i -E 's/([0-9]{2})-([0-9]{2})-([0-9]{4})/\3-\2-\1/g' ./*.txt
2
  • 1
    This won't catch all the cases. See Kusalananda's comment on my answer. Aug 26, 2021 at 15:35
  • @DanielWerner so the dates are written badly, can't do much about it. Seems like your solution is the right one here. My is more general for proper data.
    – pbies
    Aug 26, 2021 at 17:37
0

an universal approach using python: (standalone with User Input)

python3 -c 'import datetime as dt; format="%d-%m-%Y"; print(dt.datetime.strptime(input(f"input time in format {format}: "), format).strftime("%Y-%m-%d")); '
input time in format %d-%m-%Y: 9-2-1832
1832-02-09

for pipable just remove input query (single value conversion):

echo "9-2-1832"| python3 -c 'import datetime as dt; print(dt.datetime.strptime(input(), "%d-%m-%Y").strftime("%Y-%m-%d")); '
1832-02-09

and finally pipable, to convert list of string-dates:

echo -e "9-2-1832\n1-2-1234\n31-12-2020"| python3 -c 'import sys,datetime;[sys.stdout.write(datetime.datetime.strptime(line.strip(), "%d-%m-%Y").strftime("%Y-%m-%d")+"\n") for line in sys.stdin]'
1832-02-09
1234-02-01
2020-12-31

the power of this unbeautiful solution is that ANY possible formats can be used for input or output, explicitly seen and easily updated if needed, unlike hardcoded regular expressions... for instance, one can convert dates to ISO YEAR-WEEK:

echo -e "9-2-1832\n1-2-1234\n31-12-2020"| python3 -c 'import sys,datetime;[sys.stdout.write(datetime.datetime.strptime(line.strip(), "%d-%m-%Y").strftime("%G-%V")+"\n") for line in sys.stdin]'
1832-06
1234-05
2020-53

for more format specifiers see date --help

0

A little late to pitch in, but worth mentioning this awk method too:

$ echo "2021-07-19" | awk  'BEGIN{FS=OFS="-" }{print $3,$2,$1}'
19-07-2021
0

Using Raku (formerly known as Perl_6)

To start, here's a raku translation of @AdminBee's excellent awk answer:

~$ raku -e 'printf("%04d-%02d-%02d\n", .[2,1,0]) given $*IN.split("-");' <<< '19-07-2021'
2021-07-19

Raku has built-in Date routines like yyyy-mm-dd, dd-mm-yyyy, and mm-dd-yyyy. So above can more easily be written as follows (add chomp to get rid of the newline added to here-string):

~$ raku -e 'Date.new( .[2,1,0].join("-")).put given $*IN.split("-")>>.chomp;'  <<< '19-07-2021'
2021-07-19
~$ raku -e 'Date.new( .[2,1,0].join("-")).yyyy-mm-dd.put given $*IN.split("-")>>.chomp;'  <<< '19-07-2021'
2021-07-19
~$ raku -e 'Date.new( .[2,1,0].join("-")).dd-mm-yyyy.put given $*IN.split("-")>>.chomp;'  <<< '19-07-2021'
19-07-2021
~$ raku -e 'Date.new( .[2,1,0].join("-")).mm-dd-yyyy.put given $*IN.split("-")>>.chomp;'  <<< '19-07-2021'
07-19-2021

Because Raku handles ISO 8601 dates by default, using the built-in Date object type can solve a lot of issues. So if you need to subtract dates, you can do so easily:

~$  raku -e 'my $d = Date.new: .[2,1,0].join("-") given $*IN.split("-")>>.chomp;   \
            my $today = Date.today;                    \
            say "Your date is $d.";                    \
            say "Todays date is $today.";              \
            say "The difference is {$today - $d} days.";'  <<< '19-07-2021'
Your date is 2021-07-19.
Todays date is 2023-09-20.
The difference is 793 days.

FYI, "Date.today creates an object the current day according to the system clock."

https://docs.raku.org/type/Date
https://docs.raku.org/language/temporal
https://docs.raku.org/routine/sprintf
https://raku.org

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .