Hello!
I am afraid to have gone a little over the top. The lunar phase function
was implemented, as well as another function from NetHack to print following
Friday the 13th dates.
Porting the script to Ksh was OK. It was tested with AST Ksh93u+. I understand
one would generally omit Bc code because Ksh has got FP arithmetics already,
but it was left intact so it can run unders Bash. When the script code was
tested in my mobile phone Termux, Zsh emulation of Ksh didn't work because,
I believe, Zsh emulates Ksh88. It was not hard to setopt Zsh options and now
the code works with Zsh as well. The code looks a little clunky, in my
opinion, when it comes to all the hack code spread over about. Ksh93 `superior'
parameter scoping in functions was a litte hard to deal with but hopefully
the script code hacks will work correctly. Ksh code is much faster.
Most advice from c.u.shell was incorporated. There was a regression bug
in which UNIX times were not generated properly, that is now fixed. Improved
$TZ support (there were some out-of-range combinations of $TZ and date
offset which were unaccounted for). The script `calculation code' was
thoroughly tested in Ksh (see links for testing scripts in source). Sorry if
there are bugs still present. Specially, the new friday_13th() function
may still need some more work.
I will update it at my GitLab <
https://gitlab.com/mountaineerbr/scripts>
I reckon it would be cool to have a shell function to convert UNIX times to
ISO-8601 (and RFC-5322) formats.
Hyroptatyr's C-code dateutils `datediff' is required to validate the script
calculations, that is run when the debug function is set.
Many thanks to all, it meant a whole lot for the code.
#!/usr/bin/env ksh
# datediff.sh - Calculate time ranges between dates
# v0.21 nov/2022 mountaineerbr GPLv3+
[[ $BASH_VERSION ]] && shopt -s extglob #bash2.05b+/ksh93u+/zsh5+
[[ $ZSH_VERSION ]] && setopt KSH_GLOB KSH_ARRAYS SH_WORD_SPLIT
HELP="NAME
${0##*/} - Calculate time ranges/intervals between dates
SYNOPSIS
${0##*/} [-NUM] [-Rrttuvvv] [-f\"FMT\"] \"DATE1\" \"DATE2\" [UNIT]
${0##*/} -FF [-vv] [[DAY_IN_WEEK] [DAY_IN_MONTH]] [START_DATE]
${0##*/} -e [-v] YEAR..
${0##*/} -l [-v] YEAR..
${0##*/} -m [-v] DATE..
${0##*/} -h
DESCRIPTION
Calculate time interval (elapsed) between DATE1 and DATE2 in var-
ious time units. The \`date' programme is optionally run to process
dates.
Other functions include checking if YEAR is leap, Easter date on
a given YEAR and phase of the moon at DATE.
In the main function, \`GNU date' accepts mostly free format human
readable date strings. If using \`FreeBSD date', input DATE strings
must be ISO-8601, \`YYYY-MM-DDThh:mm:ss' unless option \`-f FMT' is
set to a new input time format. If \`date' programme is not avail-
able then input must be ISO-8601 formatted.
If DATE is not set, defaults to \`now'. To flag DATE as UNIX time,
prepend an at sign \`@' to it or set option -r. Stdin input sup-
ports one DATE string per line (max two lines) or two ISO-8601
DATES separated by space in a single line. Input is processed in
a best effort basis.
Output RANGES section displays intervals in different units of
time (years or months or weeks or days or hours or minutes or
seconds alone). It also displays a compound time range with all
the above units into consideration to each other.
Single UNIT time periods can be displayed in table format -t and
their scale set with -NUM where NUM is an integer. Result least
significant digit is subject to rounding. When last positional
parameter UNIT is exactly one of \`Y', \`MO', \`W', \`D', \`H',
\`M' or \`S', only a single UNIT interval is printed.
Output DATE section prints two dates in ISO-8601 format or, if
option -R is set, RFC-5322 format.
Option -e prints Easter date for given YEARs (for western churches).
Option -u sets or prints dates in Coordinated Universal Time (UTC)
in the main function.
Option -l checks if YEAR is leap. Set option -v to decrease ver-
bose. ISO-8601 system assumes proleptic Gregorian calendar, year
zero and no leap seconds.
Option -m prints lunar phase at DATE as \`YYYY[-MM[-DD]]', auto
expansion takes place on partial DATE input. DATE ought to be UTC
time. Code snippet adapted from NetHack.
Option -F prints the date of next Friday the 13th, START_DATE must
be formated as \`YYY[-MM[-DD]]'. Set twice to prints the following
10 matches. Optionally, set a day in the week, such as Sunday, and
a month day number as first and second positional parameters.
ISO-8601 DATE offset is supported throughout this script. When
environment \$TZ is a positive or negative decimal number, such
as \`UTC+3', it is read as offset. Variable \$TZ with timezone name
or ID (e.g. \`America/Sao_Paulo') is supported by \`date' programme.
This script uses Bash/Ksh arithmetics to perform most time range
calculations, as long as input is a valid ISO-8601 date format.
Option -d sets \$TZ=UTC, unsets verbose switches and run checks
against C-code \`datediff' and \`date' (dump only when results
differ), set twice to code exit only.
Option -D disables \`date' package warping and -DD disables Bash/
Ksh \`printf %()T' warping, too.
ENVIRONMENT
TZ Offset time. POSIX time zone definition by the \$TZ vari-
able takes a different form from ISO-8601 standards, so
that ISO UTC-03 is equivalent to setting \$TZ=UTC+03. Only
the \`date' programme can parse timezone names and IDS.
REFINEMENT RULES
Some date intervals can be calculated in more than one way depend-
ing on the logic used in the \`compound time range' display. We
decided to mimic hroptatyr's \`datediff' refinement rules as often
as possible.
Script error rate of the core code is estimated to be lower than
one percent after extensive testing with selected and corner-case
sample dates and times. Check script source code for details.
SEE ALSO
\`Datediff' from \`dateutils', by Hroptatyr.
<
www.fresse.org/dateutils/>
\`Units' from GNU.
<
https://www.gnu.org/software/units/>
Do calendrical savants use calculation to answer date questions?
A functional magnetic resonance imaging study, Cowan and Frith, 2009.
<
https://www.ncbi.nlm.nih.gov/pmc/articles/PMC2677581/#!po=21.1864>
Calendrical calculation, Dershowitz and Reingold, 1990
<
http://www.cs.tau.ac.il/~nachum/papers/cc-paper.pdf>
<
https://books.google.com.br/books?id=DPbx0-qgXu0C>
How many days are in a year? Manning, 1997.
<
https://pumas.nasa.gov/files/04_21_97_1.pdf>
Iana Time zone database
<
https://www.iana.org/time-zones>
Fun with Date Arithmetic (see replies)
<
https://linuxcommando.blogspot.com/2009/11/fun-with-date-arithmetic.html>
Tip: Division is but subtractions and multiplication but additions.
--Lost reference
WARRANTY
Licensed under the GNU General Public License 3 or better. This
software is distributed without support or bug corrections. Many
thanks for all whose advice improved this script from c.u.shell.
Bash2.05b+ or Ksh93u+ is required. \`Bc' or Ksh is required for
single-unit calculations. FreeBSD12+ or GNU \`date' is option-
ally required.
EXAMPLES
Leap year check
$ ${0##*/} -l 2000
$ ${0##*/} -l {1980..2000}
$ echo 2000 | ${0##*/} -l
Moon phases for January 1996
$ ${0##*/} -m 1996-01
Print following Friday, 13th
$ ${0##*/} -F
Print following Sunday, 12th after 1999
$ ${0##*/} -F sun 12 1999
Single unit time periods
$ ${0##*/} 2022-03-01T00:00:00 2022-03-01T10:10:10 m #(m)ins
$ ${0##*/} '10 years ago' mo #(mo)nths
$ ${0##*/} 1970-01-01 2000-02-02 y #(y)ears
Time ranges/intervals
$ ${0##*/} 2020-01-03T14:30:10 2020-12-24T00:00:00
$ ${0##*/} 0921-04-12 1999-01-31
$ echo 1970-01-01 2000-02-02 | ${0##*/}
$ TZ=UTC+3 ${0##*/} 2020-01-03T14:30:10-06 2021-12-30T21:00:10-03:20
\`GNU date' warping
$ ${0##*/} 'next monday'
$ ${0##*/} 2019/6/28 1Aug
$ ${0##*/} '5min 34seconds'
$ ${0##*/} 1aug1990-9month now
$ ${0##*/} -- -2week-3day
$ ${0##*/} -- \"today + 1day\" @1952292365
$ ${0##*/} -2 -- '1hour ago 30min ago'
$ ${0##*/} today00:00 '12 May 2020 14:50:50'
$ ${0##*/} '2020-01-01 - 6months' 2020-01-01
$ ${0##*/} '05 jan 2005' 'now - 43years -13 days'
$ ${0##*/} @1561243015 @1592865415
\`BSD date' warping
$ ${0##*/} -f'%m/%d/%Y' 6/28/2019 9/04/1970
$ ${0##*/} -r 1561243015 1592865415
$ ${0##*/} 200002280910.33 0003290010.00
$ ${0##*/} -- '-v +2d' '-v -3w'
OPTIONS
-[0-9] Set scale for single unit intervals.
-DDdd Debug, check help page.
-e Print Western Easter date.
-FF Print following Friday the 13th date.
-f FMT Input time format string (only with BSD \`date').
-h Print this help page.
-l Check if YEAR is leap year.
-m Print lunar phase at DATE (ISO UTC time).
-R Print human time in RFC-5322 format (verbose).
-r, -@ Input DATES are UNIX times.
-tt Table layout display of single unit intervals.
-u Set or print UTC time instead of local time.
-vvv Verbose level, change print layout of functions."
#TESTING RESULTS
#!# MAIN TESTING SCRIPT: <
https://pastebin.com/suw4Bif3>
# Hroptatyr's `man datediff' says ``refinement rules'' cover over 99% cases.
# Calculated C-code `datediff' error rate is at least 0.26% of total tested dates (compound range).
# Results differ from C-code `datediff' in the ~0.6% of all tested dates in script v0.21 (compound range).
# All differences occur with ``end-of-month vs. start-of-month'' dates, such as days `29, 30 or 31' of one date against days `1, 2 or 3' of the other date.
# Different results from C-code `datediff' in compound range are not necessarily errors in all cases and may be considered correct albeit with different refinements. This seems to be the case for most, if not all, other differences obtained in testing results.
# A bug was fixed in v0.20 in which UNIX time generationw was affected. No errors were found in range (seconds) calculation since.
#!# OFFSET AND $TZ TESTING SCRIPT: <
https://pastebin.com/ZXnHLrY8>
# Note `datediff' offset ranges between -14h and +14h.
# Offset-aware date results passed checking against `datediff' as of v0.21.
#Ksh exec time is ~2x faster than Bash (main function).
#NOTES
##Time zone / Offset support
#dbplunkett: <
https://stackoverflow.com/questions/38641982/converting-date-between-timezones-swift>
#-00:00 and +24:00 are valid and should equal to +00:00; however -0 is denormal;
#support up to `seconds' for time zone adjustment; POSIX time does not
#account for leap seconds; POSIX time zone definition by the $TZ variable
#takes a different form from ISO8601 standards; environment $TZ applies to both dates;
#it is easier to support OFFSET instead of TIME ZONE; should not support
#STD (standard) or DST (daylight saving time) in timezones, only offsets;
# America/Sao_Paulo is a TIMEZONE ID, not NAME; `Pacific Standard Time' is a tz name.
#<
https://stackoverflow.com/questions/3010035/converting-a-utc-time-to-a-local-time-zone-in-java>
#<
https://www.iana.org/time-zones>, <
https://www.w3.org/TR/NOTE-datetime>
#<
https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html>
##A year zero does not exist in the Anno Domini (AD) calendar year system
#commonly used to number years in the Gregorian calendar (nor in its
#predecessor, the Julian calendar); in this system, the year 1 BC is
#followed directly by year AD 1. However, there is a year zero in both
#the astronomical year numbering system (where it coincides with the
#Julian year 1 BC), and the ISO 8601:2004 system, the interchange standard
#for all calendar numbering systems (where year zero coincides with the
#Gregorian year 1 BC). In Proleptic Gregorian calendar, year 0000 is leap.
#<
https://docs.julialang.org/en/v1/stdlib/Dates/>
#Serge3leo -
https://stackoverflow.com/questions/26861118/rounding-numbers-with-bc-in-bash
#MetroEast -
https://askubuntu.com/questions/179898/how-to-round-decimals-using-bc-in-bash
#``Rounding is more accurate than chopping/truncation''.
#
https://wiki.math.ntnu.no/_media/ma2501/2016v/lecture1-intro.pdf
##Negative zeros have some subtle properties that will not be evident in
#most programs. A zero exponent with a nonzero mantissa is a "denormal."
#A denormal is a number whose magnitude is too small to be represented
#with an integer bit of 1 and can have as few as one significant bit.
#
https://www.lahey.com/float.htm
#globs
SEP='Tt/.:+-'
EPOCH=1970-01-01T00:00:00
GLOBOPT='@(y|mo|w|d|h|m|s|Y|MO|W|D|H|M|S)'
GLOBUTC='*(+|-)@(?([Uu])[Tt][Cc]|?([Uu])[Cc][Tt]|?([Gg])[Mm][Tt]|Z|z)' #see bug ``*?(exp)'' in bash2.05b extglob; [UG] are marked optional for another hack in this script
GLOBTZ="?($GLOBUTC)?(+|-)@(2[0-4]|?([01])[0-9])?(?(:?([0-5])[0-9]|:60)?(:?([0-5])[0-9]|:60)|?(?([0-5])[0-9]|60)?(?([0-5])[0-9]|60))"
GLOBDATE='?(+|-)+([0-9])[/.-]@(1[0-2]|?(0)[1-9])[/.-]@(3[01]|?(0)[1-9]|[12][0-9])'
GLOBTIME="@(2[0-4]|?([01])[0-9]):?(?([0-5])[0-9]|60)?(:?([0-5])[0-9]|:60)?($GLOBTZ)"
#
https://www.oreilly.com/library/view/regular-expressions-cookbook/9781449327453/ch04s07.html
#custom support for 24h clock and leap second
DAY_OF_WEEK=(Thursday Friday Saturday Sunday Monday Tuesday Wednesday)
MONTH_OF_YEAR=(January February March April May June July August September October November December)
YEAR_MONTH_DAYS=(31 28 31 30 31 30 31 31 30 31 30 31)
TIME_ISO8601_FMT='%Y-%m-%dT%H:%M:%S%z'
TIME_RFC5322_FMT='%a, %d %b %Y %H:%M:%S %z'
# Choose between GNU or BSD date
# datefun.sh [-u|-R|-v[val]|-I[fmt]] [YYY-MM-DD|@UNIX] [+OUTPUT_FORMAT]
# datefun.sh [-u|-R|-v[val]|-I[fmt]]
# By defaults, input should be ISO8601 date or UNIX time (append @).
# Option -I `fmt' may be `date', `hours', `minutes' or `seconds' (added in FreeBSD12).
# Setting environment TZ=UTC is equivalent to -u.
function datefun
{
typeset options unix_input input_fmt globtest ar chars start
input_fmt="${INPUT_FMT:-${TIME_ISO8601_FMT%??}}"
[[ $1 = -[RIv]* ]] && options="$1" && shift
if ((BSDDATE))
then globtest="*([$IFS])@($GLOBDATE?([$SEP])?(+([$SEP])$GLOBTIME)|$GLOBTIME)?([$SEP])*([$IFS])"
[[ ! $1 ]] && set --
if [[ $1 = +([0-9])?(.[0-9][0-9]) && ! $OPTF ]] #default fmt [[[[[cc]yy]mm]dd]HH]MM[.ss]
then ${DATE_CMD} ${options} -j "$@" && return
elif [[ $1 = $globtest && ! $OPTF ]] #ISO8601 variable length
then ar=(${1//[$SEP]/ })
[[ ${1//[$IFS]} = +([0-9])[:]* ]] && start=9 || start=0
((chars=(${#ar[@]}*2)+(${#ar[@]}-1) ))
${DATE_CMD} ${options} -j -f "${TIME_ISO8601_FMT:start:chars}" "${@/$GLOBUTC}" && return
fi
[[ ${1:-+%} != @(+%|@|-f)* ]] && set -- -f"${input_fmt}" "$@"
[[ $1 = @* ]] && set -- "-r${1#@}" "${@:2}"
${DATE_CMD} ${options} -j "$@"
else
[[ ${1:-+%} != @(+%|-d)* ]] && set -- -d"${unix_input}${1}" "${@:2}"
${DATE_CMD} ${options} "$@"
fi
}
#leap fun
function is_leapyear
{
((!($1 % 4) && ($1 % 100 || !($1 % 400) ) ))
}
#print the maximum number of days of a given month
#usage: month_maxday [MONTH] [YEAR]
#MONTH range 1-12; YEAR cannot be nought.
function month_maxday
{
typeset month year
month="$1" year="$2"
if ((month==2)) && is_leapyear $year
then echo 29
else echo ${YEAR_MONTH_DAYS[month-1]}
fi
}
#year days, leap years only if date1's month is before or at feb.
function year_days_adj
{
typeset month year
month="$1" year="$2"
if ((month<=2)) && is_leapyear $year
then echo 366
else echo 365
fi
}
#check if input is an integer year
function is_year
{
if [[ $1 = +([0-9]) ]]
then return 0
else printf 'err: year must be in the format YYYY -- %s\n' "$1" >&2
fi
return 1
}
#verbose check if year is leap
function is_leapyear_verbose
{
typeset year
year="$1"
if is_leapyear $year
then ((OPTVERBOSE)) || printf 'leap year -- %04d\n' $year
else ((OPTVERBOSE)) || printf 'not leap year -- %04d\n' $year
false
fi
}
#
https://stackoverflow.com/questions/32196629/my-shell-script-for-checking-leap-year-is-showing-error
#check Easter date in a given year
function easterf
{
echo ${*:?year required} '[ddsf[lfp[too early
]Pq]s@1583>@
ddd19%1+sg100/1+d3*4/12-sx8*5+25/5-sz5*4/lx-10-sdlg11*20+lz+lx-30%
d[30+]s@0>@d[[1+]s@lg11<@]s@25=@d[1+]s@24=@se44le-d[30+]s@21>@dld+7%-7+
[March ]smd[31-[April ]sm]s@31<@psnlmPpsn1z>p]splpx' | dc
}
#Dershowitz' and Reingold' Calendrical Calculations book
#get day in the week
#usage: get_day_in_week unix_time
function get_day_in_week
{
echo ${DAY_OF_WEEK[( ( ($1+($1<0?1:0))/(24*60*60))%7 +($1<0?6:7))%7]}
}
#get day in the year
#usage: get_day_in_year year month day
function get_day_in_year
{
typeset day month year month_test daysum
day="${1#0}" month="${2#0}" year="${3##+(0)}"
for ((month_test=1;month_test<month;++month_test))
do ((daysum+=$(month_maxday "$month_test" "$year")))
done
echo $((day+daysum))
}
#return phase of the moon, use UTC time
#usage: phase_of_the_moon year [month] [day]
function phase_of_the_moon #0-7, with 0: new, 4: full
{
typeset day month year diy goldn epact
day="${1#0}" month="${2#0}" year="${3##+(0)}"
day=${day:-1} month=${month:-1} year=${year:-0}
diy=$(get_day_in_year "$day" "$month" "$year")
((goldn = (year % 19) + 1))
((epact = (11 * goldn + 18) % 30))
(((epact == 25 && goldn > 11) || epact == 24 )) && ((epact++))
case $(( ( ( ( ( (diy + epact) * 6) + 11) % 177) / 22) & 7)) in
0) set -- 'New Moon' ;; #.0
1) set -- 'Waxing Crescent' ;;
2) set -- 'First Quarter' ;; #.25
3) set -- 'Waxing Gibbous' ;;
4) set -- 'Full Moon' ;; #.5
5) set -- 'Waning Gibbous' ;;
6) set -- 'Last Quarter' ;; #.75
7) set -- 'Waning Crescent' ;;
esac
#Bash's integer division truncates towards zero, as in C.
[[ $*${OPTM#2} = $PHASE_SKIP ]] && return || PHASE_SKIP="$*"
if ((OPTVERBOSE))
then printf '%s\n' "$*"
else printf '%04d-%02d-%02d %s\n' "$year" "$month" "$day" "$*"
fi
}
#<
https://nethack.org/>
#<
https://aa.usno.navy.mil/data/MoonPhases>
#<
https://aa.usno.navy.mil/faq/moon_phases>
#<
http://astropixels.com/ephemeris/phasescat/phases1901.html>
#<
https://www.nora.ai/competition/fishai-dataset-competition/about-the-dataset/>
#<
https://www.kaggle.com/datasets/lsind18/full-moon-calendar-1900-2050>
#<
https://www.fullmoon.info/en/fullmoon-calendar_1900-2050.html>
#get current time
#usage: get_timef [unix_time] [print_format]
function get_timef
{
typeset fmt
fmt="${2:-${TIME_ISO8601_FMT}}"
if ((OPTDD))
then echo $EPOCH ;false
elif [[ $ZSH_VERSION ]]
then zmodload -aF zsh/datetime b:strftime && strftime "$fmt" $1
else printf "%(${fmt})T\n" ${BASH_VERSION:+${1:--1}}
fi
}
#get friday 13th dates
#usage: friday_13th [weekday_name] [day] [start_year]
function friday_13th
{
typeset dow_name d_tgt diw_tgt day month year unix diw maxday skip n
dow_name=("${DAY_OF_WEEK[@]}") ;DAY_OF_WEEK=(0 1 2 3 4 5 6)
#set day of week and day of month
[[ $2 = [SsMmTtWwFf]* && $1 = ?([0-3])[0-9] ]] && set -- "$2" "$1" "${@:3}"
if [[ $1 = [SsMmTtWwFf]* && $2 = ?([0-3])[0-9] ]]
then case $1 in
[Ss][Aa]*) diw_tgt=${DAY_OF_WEEK[2]};;
[Ff]*) diw_tgt=${DAY_OF_WEEK[1]};;
[Tt]*) diw_tgt=${DAY_OF_WEEK[0]};;
[Ww]*) diw_tgt=${DAY_OF_WEEK[6]};;
[Tt][Uu]*) diw_tgt=${DAY_OF_WEEK[5]};;
[Mm]*) diw_tgt=${DAY_OF_WEEK[4]};;
[Ss]*) diw_tgt=${DAY_OF_WEEK[3]};;
esac
d_tgt=$2 ;shift 2
fi ;diw_tgt=${diw_tgt:-1} d_tgt=${d_tgt:-13}
[[ $1 ]] || set -- $(get_timef) ;set -- ${*//[$SEP]/ }
day="${3#0}" month="${2#0}" year="${1##+(0)}"
day="${day:-1}" month="${month:-1}" year="${year:-0}"
unix=$(GETUNIX=1 OPTVERBOSE=1 OPTRR= TZ=UTC \
mainf $EPOCH ${year}-${month}-${day}) || return $?
while diw=$(get_day_in_week $((unix+(d_away*24*60*60) )) )
do if ((diw==diw_tgt && day==d_tgt))
then if ((!(d_away+OPTVERBOSE+OPTFF-1) ))
then printf "%s, %02d %s %04d is today!\n" \
"${dow_name[diw_tgt]:0:3}" "$day" "${MONTH_OF_YEAR[month-1]:0:3}" "$year"
elif ((OPTVERBOSE))
then printf "%04d-%02d-%02d\n" "$year" "$month" "$day"
else printf "%s, %02d %s %04d is %4d days ahead\n" \
"${dow_name[diw_tgt]:0:3}" "$day" "${MONTH_OF_YEAR[month-1]:0:3}" "$year" "$d_away"
fi
((++n))
((OPTFF==1||(OPTFF==2&&n>=10) )) && break
fi
maxday=$(month_maxday $month $year)
if ((day<d_tgt))
then ((d_away=d_tgt-day, day=d_tgt, skip=1))
elif ((day>d_tgt))
then ((d_away=(maxday-day+d_tgt), day=d_tgt))
else ((d_away+=maxday))
fi
if ((!skip))
then ((month==12)) && ((++year))
((month=(month==12?1:month+1) ))
fi ;skip=
done
}
#printing helper
#(A). check if floating point in $1 is `>0', set return signal and $SS to `s' when `>1.0'.
#usage: prHelpf 1.23
#(B). set padding of $1 length until [max] chars and set $SSS.
#usage: prHelpf 1.23 [max]
function prHelpf
{
typeset val valx int dec x z
#(B)
if (($#>1))
then SSS= x=$(( ${2} - ${#1} ))
for ((z=0;z<x;++z))
do SSS="$SSS "
done
fi
#(A)
SS= val=${1#-} val=${val#0} valx=${val//[0.]} int=${val%.*}
[[ $val = *.* ]] && dec=${val#*.} dec=${dec//0}
[[ $1 && $OPTT ]] || ((valx)) || return
(( int>1 || ( (int==1) && (dec) ) )) && SS=s
return 0
}
#datediff fun
function mainf
{
${DEBUG:+unset} typeset date1_iso8601 date2_iso8601 unix1 unix2 inputA inputB range neg_range yearA monthA dayA hourA minA secA tzA neg_tzA tzAh tzAm tzAs yearB monthB dayB hourB minB secB tzB neg_tzB tzBh tzBm tzBs years_between y_test leapcount daycount_leap_years daycount_years fullmonth_days fullmonth_days_save monthcount month_test month_tgt d1_mmd d2_mmd date1_month_max_day date3_month_max_day date1_year_days_adj d_left y mo w d h m s bc bcy bcmo bcw bcd bch bcm range_pr sh d_left_save d_sum date1_iso8601_pr date2_iso8601_pr yearAtz monthAtz dayAtz hourAtz minAtz secAtz yearBtz monthBtz dayBtz hourBtz minBtz secBtz yearAprtz monthAprtz dayAprtz hourAprtz minAprtz secAprtz yearBprtz monthBprtz dayBprtz hourBprtz minBprtz secBprtz range_check now badges date1_diw date2_diw prfmt varname buf var ok ar ret n p q r v TZh TZm TZs TZ_neg TZ_pos spcr #SS SSS
(($# == 1)) && set -- '' "$1"
#warp `date' when available
if unix1=$(datefun "${1:-+%s}" ${1:++%s}) &&
unix2=$(datefun "${2:-+%s}" ${2:++%s})
then ((GETUNIX)) && { echo $((unix1+unix2)) ;unset GETUNIX ;return ${ret:-0} ;}
#sort dates
if ((unix1 > unix2))
then buf=$unix2 unix2=$unix1 unix1=$buf neg_range=-1
set -- "$2" "$1" "${@:3}"
fi
{
date1_iso8601=$(datefun -Iseconds @"$unix1")
date2_iso8601=$(datefun -Iseconds @"$unix2")
if [[ ! $OPTVERBOSE && $OPTRR ]]
then date1_iso8601_pr=$(datefun -R @"$unix1")
date2_iso8601_pr=$(datefun -R @"$unix2")
fi
} 2>/dev/null #avoid printing errs from FreeBSD<12 `date'
else unset unix1 unix2
#set default date -- AD
[[ ! $1 || ! $2 ]] && now=$(get_timef)
[[ ! $1 ]] && { set -- "${now}" "${@:2}" ;date1_iso8601="$now" ;}
[[ ! $2 ]] && { set -- "$1" "${now}" "${@:3}" ;date2_iso8601="$now" ;}
fi
#load ISO8601 dates from `date' or user input
inputA="${date1_iso8601:-$1}" inputB="${date2_iso8601:-$2}"
if [[ ! $unix2 ]] #time only input, no `date' pkg available
then [[ $inputA = *([0-9]):* ]] && inputA="${EPOCH:0:10}T${inputA}"
[[ $inputB = *([0-9]):* ]] && inputB="${EPOCH:0:10}T${inputB}"
fi
IFS="${IFS}${SEP}UuGgZz" read yearA monthA dayA hourA minA secA tzA <<<"${inputA##*(+|-)}"
IFS="${IFS}${SEP}UuGgZz" read yearB monthB dayB hourB minB secB tzB <<<"${inputB##*(+|-)}"
IFS="${IFS}${SEP/[Tt]}" read tzAh tzAm tzAs var <<<"${tzA##?($GLOBUTC?(+|-)|[+-])}"
IFS="${IFS}${SEP/[Tt]}" read tzBh tzBm tzBs var <<<"${tzB##?($GLOBUTC?(+|-)|[+-])}"
IFS="${IFS}${SEP/[Tt]}" read TZh TZm TZs var <<<"${TZ##?($GLOBUTC?(+|-)|[+-])}"
#fill in some defaults
monthA=${monthA:-1} dayA=${dayA:-1} monthB=${monthB:-1} dayB=${dayB:-1}
#support offset as `[+-]XXXX??'
[[ $tzAh = [0-9][0-9][0-9][0-9]?([0-9][0-9]) ]] \
&& tzAs=${tzAh:4:2} tzAm=${tzAh:2:2} tzAh=${tzAh:0:2}
[[ $tzBh = [0-9][0-9][0-9][0-9]?([0-9][0-9]) ]] \
&& tzBs=${tzBh:4:2} tzBm=${tzBh:2:2} tzBh=${tzBh:0:2}
[[ ${TZh} = [0-9][0-9][0-9][0-9]?([0-9][0-9]) ]] \
&& TZs=${TZh:4:2} TZm=${TZh:2:2} TZh=${TZh:0:2}
#set parameters as decimals ASAP
for varname in yearA monthA dayA hourA minA secA \
yearB monthB dayB hourB minB secB \
tzAh tzAm tzAs tzBh tzBm tzBs TZh TZm TZs
do eval "[[ \${$varname} = *[A-Za-z_]* ]] && continue" #avoid printing errs
eval "(($varname=\${$varname//[!+-]}10#0\${$varname#[+-]}))"
done
#negative years
[[ $inputA = -?* ]] && yearA=-$yearA
[[ $inputB = -?* ]] && yearB=-$yearB
#
#iso8601 date string offset
[[ ${inputA%"${tzA##?($GLOBUTC?(+|-)|[+-])}"} = *?- ]] && neg_tzA=-1 || neg_tzA=+1
[[ ${inputB%"${tzB##?($GLOBUTC?(+|-)|[+-])}"} = *?- ]] && neg_tzB=-1 || neg_tzB=+1
((tzAh==0 && tzAm==0 && tzAs==0)) && neg_tzA=+1
((tzBh==0 && tzBm==0 && tzBs==0)) && neg_tzB=+1
#
#environment $TZ
[[ ${TZ##*$GLOBUTC} = -?* ]] && TZ_neg=-1 || TZ_neg=+1
((TZh==0 && TZm==0 && TZs==0)) && TZ_neg=+1
((TZ_neg<0)) && TZ_pos=+1 || TZ_pos=-1
[[ $TZh$TZm$TZs = *([0-9+-]) && ! $unix2 ]] || unset TZh TZm TZs
#24h clock and input leap second support (these $tz* parameters will be zeroed later)
((hourA==24)) && (( (neg_tzA>0 ? (tzAh-=hourA-23) : (tzAh+=hourA-23) ) , (hourA-=hourA-23) ))
((hourB==24)) && (( (neg_tzB>0 ? (tzBh-=hourB-23) : (tzBh+=hourB-23) ) , (hourB-=hourB-23) ))
((minA==60)) && (( (neg_tzA>0 ? (tzAm-=minA-59) : (tzAm+=minA-59) ) , (minA-=minA-59) ))
((minB==60)) && (( (neg_tzB>0 ? (tzBm-=minB-59) : (tzBm+=minB-59) ) , (minB-=minB-59) ))
((secA==60)) && (( (neg_tzA>0 ? (tzAs-=secA-59) : (tzAs+=secA-59) ) , (secA-=secA-59) ))
((secB==60)) && (( (neg_tzB>0 ? (tzBs-=secB-59) : (tzBs+=secB-59) ) , (secB-=secB-59) ))
#CHECK SCRIPT `GLOBS', TOO, as they may fail with weyrd dates and formats.
#check input validity
d1_mmd=$(month_maxday "$monthA" "$yearA") ;d2_mmd=$(month_maxday "$monthB" "$yearB")
if ! (( (yearA||yearA==0) && (yearB||yearB==0) && monthA && monthB && dayA && dayB )) ||
((
monthA>12 || monthB>12 || dayA>d1_mmd || dayB>d2_mmd
|| hourA>23 || hourB>23 || minA>59 || minB>59 || secA>59 || secB>59
))
then echo "err: illegal user input -- ISO-8601 DATE required" >&2 ;return 2
fi
#offset and $TZ support
if ((tzAh||tzAm||tzAs||tzBh||tzBm||tzBs||TZh||TZm||TZs))
then #check validity
if ((tzAh>24||tzBh>24||tzAm>60||tzBm>60||tzAs>60||tzBs>60))
then echo "warning: illegal offsets" >&2
unset tzA tzB tzAh tzAm tzAs tzBh tzBm tzBs
fi
if ((TZh>23||TZm>59||TZs>59))
then echo "warning: illegal environment \$TZ" >&2
unset TZh TZm TZs
fi #offset specs:
#<
https://www.w3.org/TR/NOTE-datetime>
#<
https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html>
#environment $TZ support #only for printing
if ((!OPTVERBOSE)) && ((TZh||TZm||TZs))
then ((hourAprtz-=(TZh*TZ_neg), minAprtz-=(TZm*TZ_neg), secAprtz-=(TZs*TZ_neg) ))
((hourBprtz-=(TZh*TZ_neg), minBprtz-=(TZm*TZ_neg), secBprtz-=(TZs*TZ_neg) ))
[[ ! $tzA ]] && ((tzAh-=(TZh*TZ_neg), tzAm-=(TZm*TZ_neg), tzAs-=(TZs*TZ_neg) ))
[[ ! $tzB ]] && ((tzBh-=(TZh*TZ_neg), tzBm-=(TZm*TZ_neg), tzBs-=(TZs*TZ_neg) ))
else unset TZh TZm TZs
fi
#convert dates to UTC for internal range calculations
((tzAh||tzAm||tzAs)) && var="A" || var=""
((tzBh||tzBm||tzBs)) && var="$var B"
((TZh||TZm||TZs)) && var="$var A.pr B.pr"
for v in $var #A B A.pr B.pr
do
[[ $v = ?.* ]] && p=${v#*.} v=${v%.*} || p=
#secAtz secBtz secAprtz secBprtz
((sec${v}${p}tz=sec${v}-(tz${v}s*neg_tz${v}) )) #neg_tzA neg_tzB
if ((sec${v}${p}tz<0))
then ((min${v}${p}tz+=((sec${v}${p}tz-59)/60) , sec${v}${p}tz=(sec${v}${p}tz%60+60)%60))
elif ((sec${v}${p}tz>59))
then ((min${v}${p}tz+=(sec${v}${p}tz/60) , sec${v}${p}tz%=60))
fi
#minAtz minBtz minAprtz minBprtz
((min${v}${p}tz+=min${v}-(tz${v}m*neg_tz${v}) ))
if ((min${v}${p}tz<0))
then ((hour${v}${p}tz+=((min${v}${p}tz-59)/60) , min${v}${p}tz=(min${v}${p}tz%60+60)%60))
elif ((min${v}${p}tz>59))
then ((hour${v}${p}tz+=(min${v}${p}tz/60) , min${v}${p}tz%=60))
fi
#hourAtz hourBtz hourAprtz hourBprtz
((hour${v}${p}tz+=hour${v}-(tz${v}h*neg_tz${v}) ))
if ((hour${v}${p}tz<0))
then ((day${v}${p}tz+=((hour${v}${p}tz-23)/24) , hour${v}${p}tz=(hour${v}${p}tz%24+24)%24))
elif ((hour${v}${p}tz>23))
then ((day${v}${p}tz+=(hour${v}${p}tz/24) , hour${v}${p}tz%=24))
fi
#dayAtz dayBtz dayAprtz dayBprtz
((day${v}${p}tz+=day${v}))
if ((day${v}${p}tz<1))
then var=$(month_maxday "$((month${v}==1 ? 12 : month${v}-1))" "$((year${v}))")
((day${v}${p}tz+=var))
if ((month${v}>1))
then ((--month${v}${p}tz))
else ((month${v}${p}tz-=month${v}))
fi
elif var=$(month_maxday "$((month${v}))" "$((year${v}))")
((day${v}${p}tz>var))
then ((++month${v}${p}tz))
((day${v}${p}tz%=var))
fi
#monthAtz monthBtz monthAprtz monthBprtz
((month${v}${p}tz+=month${v}))
if ((month${v}${p}tz<1))
then ((--year${v}${p}tz))
((month${v}${p}tz+=12))
elif ((month${v}${p}tz>12))
then ((++year${v}${p}tz))
((month${v}${p}tz%=12))
fi
((year${v}${p}tz+=year${v})) #yearAtz yearBtz yearAprtz yearBprtz
done
#modulus as (a%b + b)%b to avoid negative remainder.
#<
https://www.geeksforgeeks.org/modulus-on-negative-numbers/>
if [[ $yearAtz ]]
then (( yearA=yearAtz , monthA=monthAtz , dayA=dayAtz,
hourA=hourAtz , minA=minAtz , secA=secAtz ,
tzAh=0 , tzAm=0 , tzAs=0
))
fi
if [[ $yearBtz ]]
then (( yearB=yearBtz , monthB=monthBtz , dayB=dayBtz,
hourB=hourBtz , minB=minBtz , secB=secBtz ,
tzBh=0 , tzBm=0 , tzBs=0
))
fi
if [[ $yearAprtz ]]
then date1_iso8601_pr=$(printf \
%04d-%02d-%02dT%02d:%02d:%02d%s%02d:%02d:%02d\\n \
"$yearAprtz" "$monthAprtz" "${dayAprtz}" \
"${hourAprtz}" "${minAprtz}" "${secAprtz}" \
"${TZ_pos%1}" "$TZh" "$TZm" "$TZs")
fi
if [[ $yearBprtz ]]
then date2_iso8601_pr=$(printf \
%04d-%02d-%02dT%02d:%02d:%02d%s%02d:%02d:%02d\\n \
"$yearBprtz" "$monthBprtz" "${dayBprtz}" \
"${hourBprtz}" "${minBprtz}" "${secBprtz}" \
"${TZ_pos%1}" "$TZh" "$TZm" "$TZs")
fi
elif [[ ! $unix2$OPTVERBOSE && $tzA$tzB$TZ = *+([A-Za-z_])* ]]
then #echo "warning: input DATE or \$TZ contains timezone ID or name. Support requires package \`date'" >&2
unset tzA tzB tzAh tzBh tzAm tzBm tzAs tzBs TZh TZm TZs
else unset tzA tzB tzAh tzBh tzAm tzBm tzAs tzBs TZh TZm TZs
fi #Offset is *from* UTC, while $TZ is *to* UTC.
#sort `UTC' dates (if no `date' package)
if [[ ! $unix2 ]] && ((
(yearA>yearB)
|| ( (yearA==yearB) && (monthA>monthB) )
|| ( (yearA==yearB) && (monthA==monthB) && (dayA>dayB) )
|| ( (yearA==yearB) && (monthA==monthB) && (dayA==dayB) && (hourA>hourB) )
|| ( (yearA==yearB) && (monthA==monthB) && (dayA==dayB) && (hourA==hourB) && (minA>minB) )
|| ( (yearA==yearB) && (monthA==monthB) && (dayA==dayB) && (hourA==hourB) && (minA==minB) && (secA>secB) )
))
then neg_range=-1
for varname in inputA yearA monthA dayA hourA minA secA \
yearAtz monthAtz dayAtz hourAtz minAtz secAtz \
yearAprtz monthAprtz dayAprtz hourAprtz minAprtz secAprtz \
tzA tzAh tzAm tzAs neg_tzA date1_iso8601 date1_iso8601_pr
do #swap $varA/$varB or $var1/$var2 values
[[ $varname = *A* ]] && p=A q=B || p=1 q=2
eval "buf=\"\$$varname\""
eval "$varname=\"\$${varname/$p/$q}\" ${varname/$p/$q}=\"\$buf\""
done
unset varname p q
set -- "$2" "$1" "${@:3}"
fi
##Count leap years and sum leap and non leap years days,
for ((y_test=(yearA+1);y_test<yearB;++y_test))
do
#((y_test==0)) && continue #ISO8601 counts year zero, proleptic gregorian/julian do not
is_leapyear $y_test && ((++leapcount))
((++years_between))
((monthcount += 12))
done
##count days in non and leap years
(( daycount_leap_years = (366 * leapcount) ))
(( daycount_years = (365 * (years_between - leapcount) ) ))
#date2 days so far this year (this month)
#days in prior months `this' year
((month_tgt = (yearA==yearB ? monthA : 0) ))
for ((month_test=(monthB-1);month_test>month_tgt;--month_test))
do
if ((month_test==2)) && is_leapyear $yearB
then (( fullmonth_days += 29 ))
else (( fullmonth_days += ${YEAR_MONTH_DAYS[month_test-1]} ))
fi
((++monthcount))
done
#date1 days until end of `that' year
#days in prior months `that' year
((yearA==yearB)) ||
for ((month_test=(monthA+1);month_test<13;++month_test))
do
if ((month_test==2)) && is_leapyear $yearA
then (( fullmonth_days += 29 ))
else (( fullmonth_days += ${YEAR_MONTH_DAYS[month_test-1]} ))
fi
((++monthcount))
done
((fullmonth_days_save = fullmonth_days))
#some info about input dates and their context..
date3_month_max_day=$(month_maxday "$((monthB==1 ? 12 : monthB-1))" "$yearB")
date1_month_max_day=$(month_maxday "$monthA" "$yearA")
date1_year_days_adj=$(year_days_adj "$monthA" "$yearA")
#set years and months
(( y = years_between ))
(( mo = ( monthcount - ( (years_between) ? (years_between * 12) : 0) ) ))
#days left
if ((yearA==yearB && monthA==monthB))
then
((d_left = (dayB - dayA) ))
((d_left_save = d_left))
elif ((dayA<dayB))
then
((++mo))
((fullmonth_days += date1_month_max_day))
((d_left = (dayB - dayA) ))
((d_left_save = d_left))
elif ((dayA>dayB))
then #refinement rules (or hacks)
((d_left = ( (date3_month_max_day>=dayA) ? (date3_month_max_day-dayA) : (date1_month_max_day-dayA) ) + dayB ))
((d_left_save = (date1_month_max_day-dayA) + dayB ))
if ((dayA>date3_month_max_day && date3_month_max_day<date1_month_max_day && dayB>1))
then
((dayB>=dayA-date3_month_max_day)) && ##addon2 -- prevents negative days
((d_left -= date1_month_max_day-date3_month_max_day))
((d_left==0 && ( (24-hourA)+hourB<24 || ( (24-hourA)+hourB==24 && (60-minA)+minB<60 ) || ( (24-hourA)+hourB==24 && (60-minA)+minB==60 && (60-secA)+secB<60 ) ) && (++d_left) )) ##addon3 -- prevents breaking down a full month
if ((d_left < 0))
then if ((w))
then ((--w , d_left+=7))
elif ((mo))
then ((--mo , w=date3_month_max_day/7 , d_left+=date3_month_max_day%7))
elif ((y))
then ((--y , mo+=11 , w=date3_month_max_day/7 , d_left+=date3_month_max_day%7))
fi
fi
elif ((dayA>date3_month_max_day)) #dayB==1
then
((d_left = (date1_month_max_day - dayA + date3_month_max_day + dayB) ))
((w = d_left/7 , d_left%=7))
if ((mo))
then ((--mo))
elif ((y))
then ((--y , mo+=11))
fi
fi
else #`dayA' equals `dayB'
((++mo))
((fullmonth_days += date1_month_max_day))
#((d_left_save = d_left)) #set to 0
fi
((h += (24-hourA)+hourB))
if ((h && h<24))
then if ((d_left))
then ((--d_left , ++ok))
elif ((mo))
then ((--mo , d_left+=date3_month_max_day-1 , ++ok))
elif ((y))
then ((--y , mo+=11 , d_left+=date3_month_max_day-1 , ++ok))
fi
fi
((h %= 24))
((m += (60-minA)+minB))
if ((m && m<60))
then if ((h))
then ((--h))
elif ((d_left))
then ((--d_left , h+=23 , ++ok))
elif ((mo))
then ((--mo , d_left+=date3_month_max_day-1 , h+=23 , ++ok))
elif ((y))
then ((--y , mo+=11 , d_left+=date3_month_max_day-1 , h+=23 , ++ok))
fi
fi
((m %= 60))
((s = (60-secA)+secB))
if ((s && s<60))
then if ((m))
then ((--m))
elif ((h))
then ((--h , m+=59))
elif ((d_left))
then ((--d_left , h+=23 , m+=59 , ++ok))
elif ((mo))
then ((--mo , d_left+=date3_month_max_day-1 , h+=23 , m+=59 , ++ok))
elif ((y))
then ((--y , mo+=11 , d_left+=date3_month_max_day-1 , h+=23 , m+=59 , ++ok))
fi
fi
((s %= 60))
((ok && (--d_left_save) ))
((m += s/60 , s %= 60))
((h += m/60 , m %= 60))
((d_left_save += h/24))
((d_left += h/24 , h %= 24))
((y += mo/12 , mo %= 12))
((w += d_left/7))
((d = d_left%7))
#total sum of full days { range = unix2-unix1 }
((d_sum = ( (d_left_save) + (fullmonth_days + daycount_years + daycount_leap_years) ) ))
((range = (d_sum * 3600 * 24) + (h * 3600) + (m * 60) + s))
#generate unix times arithmetically?
((GETUNIX)) && { echo ${neg_range%1}${range} ;unset GETUNIX ;return ${ret:-0} ;}
if [[ ! $unix2 ]]
then badges="$badges#"
if ((
(yearA>1970 ? yearA-1970 : 1970-yearA)
> (yearB>1970 ? yearB-1970 : 1970-yearB)
))
then var=$yearB-$monthB-${dayB}T$hourB:$minB:$secB varname=B #utc times
else var=$yearA-$monthA-${dayA}T$hourA:$minA:$secA varname=A
fi
var=$(GETUNIX=1 DATE_CMD=false OPTVERBOSE=1 OPTRR= TZ= \
mainf $EPOCH $var) || ((ret+=$?))
if [[ $varname = B ]]
then ((unix2=var , unix1=unix2-range))
else ((unix1=var , unix2=unix1+range))
fi
if ((OPTRR)) #make RFC-5322 format string
then if ! { date2_iso8601_pr=$(get_timef "$unix2" "$TIME_RFC5322_FMT") &&
date1_iso8601_pr=$(get_timef "$unix1" "$TIME_RFC5322_FMT") ;}
then #calculate Day Of Week (bash v<3.1)
date2_diw=$(get_day_in_week $((unix2-( ( (TZh*60*60)+(TZm*60)+TZs)*TZ_neg) )) )
date1_diw=$(get_day_in_week $((unix1-( ( (TZh*60*60)+(TZm*60)+TZs)*TZ_neg) )) )
date2_iso8601_pr=$(printf \
'%s, %02d %s %04d %02d:%02d:%02d %s%02d:%02d:%02d\n' \
"${date2_diw:0:3}" "${dayBprtz:-${dayBtz:-$dayB}}" \
"${MONTH_OF_YEAR[${monthBprtz:-${monthBtz:-$monthB}}-1]:0:3}" \
"${yearBprtz:-${yearBtz:-$yearB}}" \
"${hourBprtz:-${hourBtz:-$hourB}}" \
"${minBprtz:-${minBtz:-$minB}}" \
"${secBprtz:-${secBtz:-$secB}}" \
"${TZ_pos%1}" "$TZh" "$TZm" "$TZs")
date1_iso8601_pr=$(printf \
'%s, %02d %s %04d %02d:%02d:%02d %s%02d:%02d:%02d\n' \
"${date1_diw:0:3}" "${dayAprtz:-${dayAtz:-$dayA}}" \
"${MONTH_OF_YEAR[${monthAprtz:-${monthAtz:-$monthA}}-1]:0:3}" \
"${yearAprtz:-${yearAtz:-$yearA}}" \
"${hourAprtz:-${hourAtz:-$hourA}}" \
"${minAprtz:-${minAtz:-$minA}}" \
"${secAprtz:-${secAtz:-$secA}}" \
"${TZ_pos%1}" "$TZh" "$TZm" "$TZs")
fi
fi
fi
#single unit time durations (when `bc' is available)
if ((OPTT || OPTVERBOSE<3))
then if [[ $BASH_VERSION ]]
then bc=( $(bc <<<" /* round argument 'x' to 'd' digits */
define r(x, d) { auto r, s; if(0 > x) { return -r(-x, d); };
r = x + 0.5*10^-d; s = scale; scale = d; r = r*10/10;
scale = s; return r; }; scale = ($SCL + 1);
r( (${years_between:-0} + ( (${range:-0} - ( (${daycount_years:-0} + ${daycount_leap_years:-0}) * 24 * 60 * 60) ) / (${date1_year_days_adj:-0} * 24 * 60 * 60) ) ) , $SCL); /** YEARS **/
r( (${monthcount:-0} + ( (${range:-0} - (${fullmonth_days_save:-0} * 24 * 60 * 60) ) / (${date1_month_max_day:-0} * 24 * 60 * 60) ) ) , $SCL); /** MONTHS **/
r( (${range:-0} / ( 7 * 24 * 60 * 60)) , $SCL); /** WEEKS **/
r( (${range:-0} / (24 * 60 * 60)) , $SCL); /** DAYS **/
r( (${range:-0} / (60 * 60)) , $SCL); /** HOURS **/
r( (${range:-0} / 60) , $SCL); /** MINUTES **/")
)
bcy=${bc[0]} bcmo=${bc[1]} bcw=${bc[2]} bcd=${bc[3]} bch=${bc[4]} bcm=${bc[5]}
#ARRAY: 0=YEARS 1=MONTHS 2=WEEKS 3=DAYS 4=HOURS 5=MINUTES
else typeset -F $SCL bcy bcmo bcw bcd bch bcm
bcy="${years_between:-0} + ( (${range:-0} - ( (${daycount_years:-0} + ${daycount_leap_years:-0}) * 24 * 60 * 60.) ) / (${date1_year_days_adj:-0} * 24 * 60 * 60.) )" #YEARS
bcmo="${monthcount:-0} + ( (${range:-0} - (${fullmonth_days_save:-0} * 24 * 60 * 60.) ) / (${date1_month_max_day:-0} * 24 * 60 * 60.) )" #MONTHS
bcw="${range:-0} / ( 7 * 24 * 60 * 60.)" #WEEKS
bcd="${range:-0} / (24 * 60 * 60.)" #DAYS
bch="${range:-0} / (60 * 60.)" #HOURS
bcm="${range:-0} / 60." #MINUTES
fi
#choose layout of single units
if ((OPTT || !OPTLAYOUT))
then #layout one
spcr=' | ' #spacer
prHelpf ${OPTTy:+${bcy}} && range_pr="${bcy} year$SS"
prHelpf ${OPTTmo:+${bcmo}} && range_pr="${range_pr}${range_pr:+$spcr}${bcmo} month$SS"
prHelpf ${OPTTw:+${bcw}} && range_pr="${range_pr}${range_pr:+$spcr}${bcw} week$SS"
prHelpf ${OPTTd:+${bcd}} && range_pr="${range_pr}${range_pr:+$spcr}${bcd} day$SS"
prHelpf ${OPTTh:+${bch}} && range_pr="${range_pr}${range_pr:+$spcr}${bch} hour$SS"
prHelpf ${OPTTm:+${bcm}} && range_pr="${range_pr}${range_pr:+$spcr}${bcm} min$SS"
prHelpf $range ;((!OPTT||OPTTs)) && range_pr="$range_pr${range_pr:+$spcr}$range sec$SS"
((OPTT&&OPTV)) && range_pr="${range_pr%%*([$IFS])}" #bug in ksh93u+ ${var% *}
else #layout two
((n = ${#range}+SCL+1)) #range in seconds is the longest string
prHelpf ${bcy} $n && range_pr=Year$SS$'\t'$SSS${bcy}
prHelpf ${bcmo} $n && range_pr="$range_pr"$'\n'Month$SS$'\t'$SSS${bcmo}
prHelpf ${bcw} $n && range_pr="$range_pr"$'\n'Week$SS$'\t'$SSS${bcw}
prHelpf ${bcd} $n && range_pr="$range_pr"$'\n'Day$SS$'\t'$SSS${bcd}
prHelpf ${bch} $n && range_pr="$range_pr"$'\n'Hour$SS$'\t'$SSS${bch}
prHelpf ${bcm} $n && range_pr="$range_pr"$'\n'Min$SS$'\t'$SSS${bcm}
prHelpf $range $((n - (SCL>0 ? (SCL+1) : 0) ))
range_pr="$range_pr"$'\n'Sec$SS$'\t'$SSS$range
range_pr="${range_pr#*([$IFS])}"
#
https://www.themathdoctors.org/should-we-put-zero-before-a-decimal-point/
((OPTLAYOUT>1)) && { p= q=. ;for ((p=0;p<SCL;++p)) ;do q="${q}0" ;done
range_pr="${range_pr// ./0.}" range_pr="${range_pr}${q}" ;}
fi
unset SS SSS
fi
#set printing array with shell results
sh=("$y" "$mo" "$w" "$d" "$h" "$m" "$s")
((y<0||mo<0||w<0||d<0||h<0||m<0||s<0)) && ret=${ret:-1} #negative unit error
# Debugging
if ((DEBUG))
then
#!#
debugf "$@"
fi
#print results
if ((!OPTVERBOSE))
then if [[ ! $date1_iso8601_pr$date1_iso8601 ]]
then date1_iso8601=$(printf \
%04d-%02d-%02dT%02d:%02d:%02d%s%02d:%02d:%02d\\n \
"$yearA" "$monthA" "$dayA" \
"$hourA" "$minA" "$secA" \
"${neg_tzA%1}" "$tzAh" "$tzAm" "$tzAs")
date1_iso8601=${date1_iso8601%%*(:00)}
else date1_iso8601_pr=${date1_iso8601_pr%%*(:00)} #remove excess zeroes
fi
if [[ ! $date2_iso8601_pr$date2_iso8601 ]]
then date2_iso8601=$(printf \
%04d-%02d-%02dT%02d:%02d:%02d%s%02d:%02d:%02d\\n \
"$yearB" "$monthB" "$dayB" \
"$hourB" "$minB" "$secB" \
"${neg_tzB%1}" "$tzBh" "$tzBm" "$tzBs")
date2_iso8601=${date2_iso8601%%*(:00)}
else date2_iso8601_pr=${date2_iso8601_pr%%*(:00)}
fi
printf '%s%s\n%s%s%s\n%s%s%s\n%s\n' \
DATES "${OPTDD+#}${badges}${neg_range%1}" \
"${date1_iso8601_pr:-${date1_iso8601:-$inputA}}" ''${unix1:+$'\t'} "$unix1" \
"${date2_iso8601_pr:-${date2_iso8601:-$inputB}}" ''${unix2:+$'\t'} "$unix2" \
RANGES
fi
prfmt='%dY %02dM %02dW %02dD %02dh %02dm %02ds' #print format for the compound range
((OPTVERBOSE>3)) && prfmt='%dY%02dM%02dW%02dD%02dh%02dm%02ds' #AST `date -E' style
((OPTVERBOSE<2 || OPTVERBOSE>2)) && printf "${prfmt}\n" "${sh[@]}"
((OPTVERBOSE<3)) && printf '%s\n' "${range_pr:-$range secs}"
return ${ret:-0}
}
#execute result checks against `datediff' and `date'
#check manually in case of divergence as this function is overloaded
#beware of opt -R and unset $TZ and offsets (we defaults to UTC while `date' may set random offsets)
function debugf
{
unset iA iB tA tB dd ddout y_dd mo_dd w_dd d_dd h_dd m_dd s_dd range_check unix1t unix2t checkA_pr checkB_pr checkA_pr_dow checkB_pr_dow checkA_utc checkB_utc date_cmd_save TZ_save brk
date_cmd_save="${DATE_CMD}" DATE_CMD=date TZ_save=$TZ TZ=UTC${TZ##*$GLOBUTC}
[[ $2 = *[Tt:]*[+-]$GLOBTZ && $1 = *[Tt:]*[+-]$GLOBTZ ]] || echo warning: input dates are missing offset/tz bits! >&2
iB="${2:-${inputB}}" iA="${1:-${inputA}}"
iB="${iB:0:25}" iA="${iA:0:25}"
((${#iB}==10)) && iB=${iB}T00:00:00
((${#iA}==10)) && iA=${iA}T00:00:00
((${#iB}==19)) && iB="${iB}+00:00"
((${#iA}==19)) && iA="${iA}+00:00"
iB=${iB/-00:00/+00:00} iA=${iA/-00:00/+00:00}
#utc time strings
tB=$(printf \
%04d-%02d-%02dT%02d:%02d:%02d%s%02d:%02d\\n \
"$yearB" "$monthB" "$dayB" \
"$hourB" "$minB" "$secB" \
"${neg_tzB%1}" $tzBh $tzBm)
tA=$(printf \
%04d-%02d-%02dT%02d:%02d:%02d%s%02d:%02d\\n \
"$yearA" "$monthA" "$dayA" \
"$hourA" "$minA" "$secA" \
"${neg_tzA%1}" $tzAh $tzAm)
tB=${tB:0:25} tA=${tA:0:25}
tB=${tB/-00:00/+00:00} tA=${tA/-00:00/+00:00}
if [[ $date_cmd_save = false ]]
then
if ((TZs)) || [[ $TZ = *:*:*:* ]] || [[ $tzA = *:*:*:* ]] || [[ $tzB = *:*:*:* ]]
then echo "warning: \`datediff' and \`date' may not take offsets with seconds" >&2
((ret+=230))
fi
if ((TZh||TZm))
then checkB_pr=$(datefun -Iseconds $iB)
checkA_pr=$(datefun -Iseconds $iA)
else checkB_pr=$date2_iso8601_pr checkA_pr=$date1_iso8601_pr
fi
if ((OPTRR))
then checkB_pr_dow=$(datefun "$iB")
checkA_pr_dow=$(datefun "$iA")
fi
checkB_utc=$(TZ=UTC datefun -Iseconds $iB)
checkA_utc=$(TZ=UTC datefun -Iseconds $iA)
#`date' iso offset must not exceed minute precision [+-]XX:XX !
#check generated unix times against `date'
unix2t=$(datefun "$iB" +%s)
unix1t=$(datefun "$iA" +%s)
range_check=$((unix2t-unix1t))
fi
if ((OPTRR))
then checkB_pr_dow="${checkB_pr_dow:-$date2_iso8601_pr}"
checkA_pr_dow="${checkA_pr_dow:-$date1_iso8601_pr}"
fi
#compound range check against `datediff'
#`datediff' offset range is between -14h and +14h!
ddout=$(datediff -f'%Y %m %w %d %H %M %S' "$tA" "$tB") || ((ret+=250))
read y_dd mo_dd w_dd d_dd h_dd m_dd s_dd <<<"$ddout"
dd=(${y_dd#-} $mo_dd $w_dd $d_dd $h_dd $m_dd $s_dd)
DATE_CMD="$date_cmd_save" TZ=$TZ_save
{
{
{ [[ ${date2_iso8601_pr:0:25} = $checkB_pr ]] &&
[[ ${date1_iso8601_pr:0:25} = $checkA_pr ]] ;} ||
{ [[ ${date2_iso8601_pr:0:3} = ${checkB_pr_dow:0:3} ]] &&
[[ ${date1_iso8601_pr:0:3} = ${checkA_pr_dow:0:3} ]] ;}
} &&
[[ $tB = ${checkB_utc:-$tB} ]] &&
[[ $tA = ${checkA_utc:-$tA} ]] &&
[[ $unix1 = ${unix1t:-$unix1} && $unix2 = ${unix2t:-$unix2} ]] &&
[[ $range = "${range_check:-$range}" ]] &&
[[ ${sh[*]} = "${dd[*]:-${sh[*]}}" ]]
} || { #brk='\n'
echo -ne "\033[2K" >&2
echo ${brk+-e} "\
sh=${sh[*]} dd=${dd[*]} | $brk"\
"$iA $iB | $brk"\
"${range:-unavail} ${range_check:-unavail} | $brk"\
"${date1_iso8601_pr:0:25} $checkA_pr | $brk"\
"${date2_iso8601_pr:0:25} $checkB_pr | $brk"\
"${date1_iso8601_pr:0:3} ${checkA_pr_dow:0:3} | $brk"\
"${date2_iso8601_pr:0:3} ${checkB_pr_dow:0:3} | $brk"\
"$tB $checkB_utc | $brk"\
"$tA $checkA_utc | $brk"\
"$unix1 $unix1t | $brk"\
"$unix2 $unix2t | $brk"\
"${date_cmd_save%date}"
((ret+=1))
}
#((DEBUG>1)) && return ${ret:-0} #!#
((DEBUG>1)) && exit ${ret:-0} #!#
return 0
}
## Parse options
while getopts 01234567890DdeFf:hlmRr@tuv opt
do case $opt in
[0-9]) SCL="$SCL$opt"
;;
d) ((++DEBUG))
;;
D) [[ ${DATE_CMD} = false ]] && OPTDD=1 ;DATE_CMD=false
;;
e) OPTE=1 OPTL=
;;
F) ((++OPTFF))
;;
f) INPUT_FMT="$OPTARG" OPTF=1 #input format string for `BSD date'
;;
h) while read
do [[ "$REPLY" = \#\ v* ]] && echo "$REPLY $SHELL" && break
done <"$0"
echo "$HELP" ;exit
;;
l) OPTL=1 OPTE=
;;
m) OPTM=1
;;
R) OPTRR=1
;;
r|@) OPTR=1
;;
t) ((++OPTLAYOUT))
;;
u) OPTU=1
;;
v) ((++OPTVERBOSE, ++OPTV))
;;
\?) exit 1
;;
esac
done
shift $((OPTIND -1)); unset opt
#set proper environment!
SCL="${SCL:-1}" #scale defaults
((OPTU)) && TZ=UTC #set UTC time zone
export TZ
#test for BSD or GNU date for datefun()
[[ ${DATE_CMD} ]] ||
if DATE_CMD=date;
! ${DATE_CMD} --version
then if gdate --version
then DATE_CMD=gdate
elif command -v date
then BSDDATE=1
else DATE_CMD=false
fi
fi >/dev/null 2>&1
#stdin input (skip it for option -F)
[[ ${1//[$IFS]}$OPTFF = $GLOBOPT ]] && opt="$1" && shift
if ((!($#+OPTFF) )) && [[ ! -t 0 ]]
then
globtest="*([$IFS])@($GLOBDATE?(+([$SEP])$GLOBTIME)|$GLOBTIME)*([$IFS])@($GLOBDATE?(+([$SEP])$GLOBTIME)|$GLOBTIME)?(+([$IFS])$GLOBOPT)*([$IFS])" #glob for two ISO8601 dates and possibly pos arg option for single unit range
while IFS= read -r || [[ $REPLY ]]
do ar=($REPLY) ;((${#ar[@]})) || continue
if ((!$#))
then set -- "$REPLY" ;((OPTL)) && break
#check if arg contains TWO ISO8601 dates and break
if ((${#ar[@]}==3||${#ar[@]}==2)) && [[ \ $REPLY = @(*[$IFS]$GLOBOPT*|$globtest) ]]
then set -- $REPLY ;[[ $1 = $GLOBOPT ]] || break
fi
else if ((${#ar[@]}==2)) && [[ \ $REPLY = @(*[$IFS]$GLOBOPT|$globtest) ]]
then set -- "$@" $REPLY
else set -- "$@" "$REPLY"
fi ;break
fi
done ;unset ar globtest REPLY
[[ ${1//[$IFS]} = $GLOBOPT ]] && opt="$1" && shift
fi
[[ $opt ]] && set -- "$@" "$opt"
#set single time unit
opt="${opt:-${@: -1}}" opt="${opt//[$IFS]}"
if [[ $opt$OPTFF = $GLOBOPT ]]
then OPTT=1 OPTVERBOSE=2 OPTLAYOUT=
case $opt in
[yY]) OPTTy=1;;
[mM][oO]) OPTTmo=1;;
[wW]) OPTTw=1;;
[dD]) OPTTd=1;;
[hH]) OPTTh=1;;
[mM]) OPTTm=1;;
[sS]) OPTTs=1;;
esac ;set -- "${@:1:$#-1}"
else OPTTy=1 OPTTmo=1 OPTTw=1 OPTTd=1 OPTTh=1 OPTTm=1 OPTTs=1
fi ;unset opt
#caveat: `gnu date' understands `-d[a-z]', do `-d[a-z]0' to pass.
#whitespace trimming
if (($#>1))
then set -- "${1#"${1%%[!$IFS]*}"}" "${2#"${2%%[!$IFS]*}"}" "${@:3}"
set -- "${1%"${1##*[!$IFS]}"}" "${2%"${2##*[!$IFS]}"}" "${@:3}"
elif (($#))
then set -- "${1#"${1%%[!$IFS]*}"}" ;set -- "${1%"${1##*[!$IFS]}"}"
fi
if ((OPTL))
then for YEAR
do is_year "$YEAR" || continue
if ! is_leapyear_verbose "$YEAR"
then (($?>1)) && RET=2 ;RET="${RET:-$?}"
fi
done ;exit $RET
elif ((OPTE))
then for YEAR
do is_year "$YEAR" || continue
DATE=$(easterf "$YEAR") ;echo $DATE
done
elif ((OPTM))
then for DATE_Y #fill in months and days
do if [[ $DATE_Y = +([0-9]) ]]
then set -- ;OPTM=2
for ((M=1;M<=12;++M)) ;do set -- "$@" "${DATE_Y}-$M" ;done
else set -- "$DATE_Y" ;PHASE_SKIP=
fi
for DATE_M
do if [[ $DATE_M = +([0-9])[$SEP]+([0-9]) ]]
then set -- ;OPTM=2
DMAX=$(month_maxday "${DATE_M#*[$SEP]}" "${DATE_M%[$SEP]*}")
for ((D=1;D<=DMAX;++D)) ;do set -- "$@" "${DATE_M}-$D" ;done
else set -- "$DATE_M" ;PHASE_SKIP=
fi
for DATE
do set -- ${DATE//[$SEP]/ } #input is ISO8601
phase_of_the_moon "$3" "$2" "$1"
done
done
done
elif ((OPTFF))
then friday_13th "$@"
else
#-r, unix times
if ((OPTR && $#>1))
then set -- @"${1#@}" @"${2#@}" "${@:3}"
elif ((OPTR && $#))
then set -- @"${1#@}"
fi
mainf "$@"
fi