How to convert from day of year and year to a date YYYYMMDD?
Solution 1
This Bash function works for me on a GNU-based system:
jul () { date -d "$1-01-01 +$2 days -1 day" "+%Y%m%d"; }
Some examples:
$ y=2011; od=0; for d in {-4..4} 59 60 {364..366} 425 426; do (( d > od + 1)) && echo; printf "%3s " $d; jul $y $d; od=$d; done
-4 20101227
-3 20101228
-2 20101229
-1 20101230
0 20101231
1 20110101
2 20110102
3 20110103
4 20110104
59 20110228
60 20110301
364 20111230
365 20111231
366 20120101
425 20120229
426 20120301
This function considers Julian day zero to be the last day of the previous year.
And here is a bash function for UNIX-based systems, such as macOS:
jul () { (( $2 >=0 )) && local pre=+; date -v$pre$2d -v-1d -j -f "%Y-%m-%d" $1-01-01 +%Y%m%d; }
Solution 2
Can't be done in just Bash, but if you have Perl:
use POSIX;
my ($jday, $year) = (100, 2011);
# Unix time in seconds since Jan 1st 1970
my $time = mktime(0,0,0, $jday, 0, $year-1900);
# same thing as a list that we can use for date/time formatting
my @tm = localtime $time;
my $yyyymmdd = strftime "%Y%m%d", @tm;
Solution 3
Run info 'Date input formats'
to see what formats are allowed.
The YYYY-DDD date format does not seem to be there, and trying
$ date -d '2011-011'
date: invalid date `2011-011'
shows it doesn't work, so I think njd
is correct, the best way is to use an external tool other than bash
and date
.
If you really want to use only bash and basic command line tools, you could do something like this:
julian_date_to_yyyymmdd()
{
date=$1 # assume all dates are in YYYYMMM format
year=${date%???}
jday=${date#$year}
for m in `seq 1 12`; do
for d in `seq 1 31`; do
yyyymmdd=$(printf "%d%02d%02d" $year $m $d)
j=$(date +"%j" -d "$yyyymmdd" 2>/dev/null)
if test "$jday" = "$j"; then
echo "$yyyymmdd"
return 0
fi
done
done
echo "Invalid date" >&2
return 1
}
But that's a pretty slow way to do it.
A faster but more complex way tries to loop over each month, finds the last day in that month, then sees if the Julian day is in that range.
# year_month_day_to_jday <year> <month> <day> => <jday>
# returns 0 if date is valid, non-zero otherwise
# year_month_day_to_jday 2011 2 1 => 32
# year_month_day_to_jday 2011 1 32 => error
year_month_day_to_jday()
{
# XXX use local or typeset if your shell supports it
_s=$(printf "%d%02d%02d" "$1" "$2" "$3")
date +"%j" -d "$_s"
}
# last_day_of_month_jday <year> <month>
# last_day_of_month_jday 2011 2 => 59
last_day_of_month_jday()
{
# XXX use local or typeset if you have it
_year=$1
_month=$2
_day=31
# GNU date exits with 0 if day is valid, non-0 if invalid
# try counting down from 31 until we find the first valid date
while test $_day -gt 0; do
if _jday=$(year_month_day_to_jday $_year $_month $_day 2>/dev/null); then
echo "$_jday"
return 0
fi
_day=$((_day - 1))
done
echo "Invalid date" >&2
return 1
}
# first_day_of_month_jday <year> <month>
# first_day_of_month_jday 2011 2 => 32
first_day_of_month_jday()
{
# XXX use local or typeset if you have it
_year=$1
_month=$2
_day=1
if _jday=$(year_month_day_to_jday $_year $_month 1); then
echo "$_jday"
return 0
else
echo "Invalid date" >&2
return 1
fi
}
# julian_date_to_yyyymmdd <julian day> <4-digit year>
# e.g. julian_date_to_yyyymmdd 32 2011 => 20110201
julian_date_to_yyyymmdd()
{
jday=$1
year=$2
for m in $(seq 1 12); do
endjday=$(last_day_of_month_jday $year $m)
if test $jday -le $endjday; then
startjday=$(first_day_of_month_jday $year $m)
d=$((jday - startjday + 1))
printf "%d%02d%02d\n" $year $m $d
return 0
fi
done
echo "Invalid date" >&2
return 1
}
Solution 4
If day of year (1-366) is 149 and year is 2014,
$ date -d "148 days 2014-01-01" +"%Y%m%d"
20140529
Be sure to input the day of year -1 value.
Solution 5
On a POSIX terminal:
jul () { date -v$1y -v1m -v1d -v+$2d -v-1d "+%Y%m%d"; }
Then call like
jul 2011 012
jul 2017 216
jul 2100 60
Related videos on Youtube
Comments
-
tiaga almost 2 years
I'm looking to go from day of year (1-366) and year (e.g. 2011) to a date in the format YYYYMMDD?
-
Jjames over 13 yearsI'm actually pretty sure that his can be done in Bash only, though not that elegant (worst case calculating the timestamp formatting it). But I'm not in front of a Linux machine to test it out.
-
Nethan over 13 years
last_day_of_month_jday
could also be implemented using e.g.date -d "$yyyymm01 -1 day"
(GNU date only) or$(($(date +"%s" -d "$yyyymm01") - 86400))
. -
Nethan over 13 yearsUsing the
date
command can be done. See my answer. But the Perl way is much easier and faster. -
user1686 over 13 years@bandi: Unless
date -d
-
Dennis Williamson over 13 yearsBash can do decrement like this:
((day--))
. Bash hasfor
loops like thisfor ((m=1; m<=12; m++))
(no need forseq
). It's pretty safe to assume that shells that have some of the other features you're using havelocal
. -
Nethan over 13 yearsThat's an awesome solution. GNU date only, but so much shorter.
-
Nethan over 13 yearsIIRC local isn't specified by POSIX, but absolutely, if using ksh has typeset, zsh has local, and zsh has declare IIRC. I think typeset works in all 3, but doesn't work in ash/dash. I do tend to underuse ksh-style for. Thanks for the thoughts.
-
Dennis Williamson over 13 years@Mikel: Dash has
local
as does BusyBoxash
. -
Nethan over 13 yearsYes, but not typeset IIRC. All others have typeset. If dash had typeset too, I would have used that in the example.
-
njd over 13 yearsI never knew GNU date was that flexible. Fantastic.
-
mr.octobor over 13 yearsgrawity: uoh, you're completely right
-
Jjames over 13 years@bandi: Please place an @ in front of a name.
-
Erich over 6 yearsfyi, the actual leap year calculation is a little more complex than "is divisible by 4".
-
Dave Lydick over 6 years@erich - You're correct, but within the range of years allowed as valid by this script (1901-2099), situations that don't work will not occur. It's not terribly difficult to add an "or" test to deal with years that are evenly divisible by 100 but not evenly divisible by 400 to cover those cases if the user needs to extend this, but I didn't feel that was really needed in this case. Perhaps that was short-sighted of me?
-
mcantsin almost 5 yearsWhat a nice solution using GNU date. In case you want to do the same on a UNIX system, such as macOS, you can use:
jul () { date -v+$2d -v-1d -j -f "%Y-%m-%d" $1-01-01 +%Y%m%d; }
-
Dennis Williamson almost 5 years@mcantsin: Thanks for the MacOS version!
-
mcantsin almost 5 years@DennisWilliamson any time ;) - thank you for your straight forward idea. This was exactly what I was looking for. Actually just needed to have it in a script that I can run on Linux and macOS.
-
Dennis Williamson almost 5 years@mcantsin: I modified your version so it works with negative offsets.
-
mcantsin almost 5 years@DennisWilliamson 👍