12

I'm looking to go from day of year (1-366) and year (e.g. 2011) to a date in the format YYYYMMDD?

1
  • 1
    What day does day 0 represent? Normally it's 1-366.
    – Mikel
    Commented Jan 11, 2011 at 13:41

9 Answers 9

18

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; }
7
  • 2
    That's an awesome solution. GNU date only, but so much shorter.
    – Mikel
    Commented Jan 12, 2011 at 4:13
  • I never knew GNU date was that flexible. Fantastic.
    – njd
    Commented Jan 12, 2011 at 13:31
  • What 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; }
    – mcantsin
    Commented Jul 13, 2019 at 5:08
  • 1
    @mcantsin: Thanks for the MacOS version! Commented Jul 13, 2019 at 5:28
  • 1
    @mcantsin: I modified your version so it works with negative offsets. Commented Jul 13, 2019 at 13:38
4

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;
7
  • 1
    I'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.
    – Bobby
    Commented Jan 11, 2011 at 10:10
  • Bobby, you might think of the date command in the coreutils. I checked it and it only supports formatting the current date or setting it to a new value, so it is not an option.
    – bandi
    Commented Jan 11, 2011 at 13:45
  • Using the date command can be done. See my answer. But the Perl way is much easier and faster.
    – Mikel
    Commented Jan 11, 2011 at 13:49
  • 2
    @bandi: Unless date -d Commented Jan 11, 2011 at 14:24
  • +1: Was using this - thanks, but the date -d method above appeared. Commented Jan 13, 2011 at 14:21
4

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
}
5
  • 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)).
    – Mikel
    Commented Jan 11, 2011 at 13:43
  • Bash can do decrement like this: ((day--)). Bash has for loops like this for ((m=1; m<=12; m++)) (no need for seq). It's pretty safe to assume that shells that have some of the other features you're using have local. Commented Jan 12, 2011 at 3:55
  • IIRC 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.
    – Mikel
    Commented Jan 12, 2011 at 4:18
  • @Mikel: Dash has local as does BusyBox ash. Commented Jan 12, 2011 at 4:46
  • Yes, but not typeset IIRC. All others have typeset. If dash had typeset too, I would have used that in the example.
    – Mikel
    Commented Jan 12, 2011 at 4:59
3

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.

1

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
0

My solution in bash

from_year=2013
from_day=362

to_year=2014
to_day=5

now=`date +"%Y/%m/%d" -d "$from_year/01/01 + $from_day days - 2 day"`
end=`date +"%Y/%m/%d" -d "$to_year/01/01 + $to_day days - 1 day"`


while [ "$now" != "$end" ] ; 
do
    now=`date +"%Y/%m/%d" -d "$now + 1 day"`;
    echo "$now";
    calc_day=`date -d "$now" +%G'.'%j`
    echo $calc_day
done
0

I realize this was asked ages ago, but none of the answers I see are what I consider pure BASH since they all use GNU date. I thought I would take a stab at answering... But I warn you ahead of time, the answer is not elegant, nor is it short at over 100 lines. It could be pared down, but I wanted to let others easily see what it does.

The primary "tricks" here are figuring out if a year is a leap year (or not) by getting the MODULUS % of the year divided by 4, and then just adding up the days in each month, plus an extra day for February if needed, using a simple table of values.

Please feel free to comment and offer suggestions on ways to do this better, since I'm primarily here to learn more myself, and I would consider myself a BASH novice at best. I did my best to make this as portable as I know how, and that means some compromises in my opinion.

On to the code... I hope it is pretty self-explanatory.

#!/bin/sh

# Given a julian day of the year and a year as ddd yyyy, return
# the values converted to yyyymmdd format using ONLY bash:

ddd=$1
yyyy=$2
if [ "$ddd" = "" ] || [ "$yyyy" = "" ]
then
  echo ""
  echo "   Usage: <command> 123 2016"
  echo ""
  echo " A valid julian day from 1 to 366 is required as the FIRST"
  echo " parameter after the command, and a valid 4-digit year from"
  echo " 1901 to 2099 is required as the SECOND. The command and each"
  echo " of the parameters must be separated from each other with a space."
  echo " Please try again."
  exit
fi
leap_yr=$(( yyyy % 4 ))
if [ $leap_yr -ne 0 ]
then
  leap_yr=0
else
  leap_yr=1
fi
last_doy=$(( leap_yr + 365 ))
while [ "$valid" != "TRUE" ]
do
  if [ 0 -lt "$ddd" ] && [ "$last_doy" -ge "$ddd" ]
  then
    valid="TRUE"
  else
    echo "   $ddd is an invalid julian day for the year given."
    echo "   Please try again with a number from 1 to $last_doy."
    exit    
  fi
done
valid=
while [ "$valid" != "TRUE" ]
do
  if [ 1901 -le "$yyyy" ] && [ 2099 -ge "$yyyy" ]
  then
    valid="TRUE"
  else
    echo "   $yyyy is an invalid year for this script."
    echo "   Please try again with a number from 1901 to 2099."
    exit    
  fi
done
if [ "$leap_yr" -eq 1 ]
then
  jan=31  feb=60  mar=91  apr=121 may=152 jun=182
  jul=213 aug=244 sep=274 oct=305 nov=335
else
  jan=31  feb=59  mar=90  apr=120 may=151 jun=181
  jul=212 aug=243 sep=273 oct=304 nov=334
fi
if [ "$ddd" -gt $nov ]
then
  mm="12"
  dd=$(( ddd - nov ))
elif [ "$ddd" -gt $oct ]
then
  mm="11"
  dd=$(( ddd - oct ))
elif [ "$ddd" -gt $sep ]
then
  mm="10"
  dd=$(( ddd - sep ))
elif [ "$ddd" -gt $aug ]
then
  mm="09"
  dd=$(( ddd - aug ))
elif [ "$ddd" -gt $jul ]
then
  mm="08"
  dd=$(( ddd - jul ))
elif [ "$ddd" -gt $jun ]
then
  mm="07"
  dd=$(( ddd - jun ))
elif [ "$ddd" -gt $may ]
then
  mm="06"
  dd=$(( ddd - may ))
elif [ "$ddd" -gt $apr ]
then
  mm="05"
  dd=$(( ddd - apr ))
elif [ "$ddd" -gt $mar ]
then
  mm="04"
  dd=$(( ddd - mar ))
elif [ "$ddd" -gt $feb ]
then
  mm="03"
  dd=$(( ddd - feb ))
elif [ "$ddd" -gt $jan ]
then
  mm="02"
  dd=$(( ddd - jan ))
else
  mm="01"
  dd="$ddd"
fi
if [ ${#dd} -eq 1 ]
then
  dd="0$dd"
fi
if [ ${#yyyy} -lt 4 ]
then
  until [ ${#yyyy} -eq 4 ]
  do
    yyyy="0$yyyy"
  done
fi
printf '\n   %s%s%s\n\n' "$yyyy" "$mm" "$dd"
2
  • fyi, the actual leap year calculation is a little more complex than "is divisible by 4".
    – Erich
    Commented Feb 28, 2018 at 2:48
  • @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? Commented Mar 1, 2018 at 4:05
0
OLD_JULIAN_VAR=$(date -u -d 1840-12-31 +%s)

TODAY_DATE=`date --date="$odate" +"%Y-%m-%d"`
TODAY_DATE_VAR=`date -u -d "$TODAY_DATE" +"%s"`
export JULIAN_DATE=$((((TODAY_DATE_VAR - OLD_JULIAN_VAR))/86400))
echo $JULIAN_DATE

mathematically represented below

[(date in sec)-(1840-12-31 in sec)]/(sec in a day 86400)
0

Another solution using awk instead of bash (which is more permissive with day ranges): mktime($1 " 01 " $2 " 00 00 00")

Example below:

$ echo -e "2023 001\n2023 031\n2023 032" | awk '{print($0 ": " strftime("%Y%m%d", mktime($1 " 01 " $2 " 00 00 00")))}'
2023 001: 20230101
2023 031: 20230131
2023 032: 20230201

You must log in to answer this question.

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