Rocksolid Light

Welcome to RetroBBS

mail  files  register  newsreader  groups  login

Message-ID:  

Maybe it's time to break that. -- Larry Wall in <199710311718.JAA19082@wall.org>


computers / comp.unix.shell / Datediff script

Datediff script

<tkju7j$54v$1@gioia.aioe.org>

  copy mid

https://www.rocksolidbbs.com/computers/article-flat.php?id=5824&group=comp.unix.shell#5824

  copy link   Newsgroups: comp.unix.shell
Path: i2pn2.org!i2pn.org!usenet.blueworldhosting.com!feed1.usenet.blueworldhosting.com!tncsrv06.tnetconsulting.net!news.snarked.org!aioe.org!svGMjj4JAseXRzIUqGFGng.user.46.165.242.75.POSTED!not-for-mail
From: no@where.com (castAway)
Newsgroups: comp.unix.shell
Subject: Datediff script
Date: Thu, 10 Nov 2022 19:33:21 -0300
Organization: Aioe.org NNTP Server
Message-ID: <tkju7j$54v$1@gioia.aioe.org>
Mime-Version: 1.0
Content-Type: text/plain; charset=UTF-8; format=flowed
Content-Transfer-Encoding: 7bit
Injection-Info: gioia.aioe.org; logging-data="5279"; posting-host="svGMjj4JAseXRzIUqGFGng.user.gioia.aioe.org"; mail-complaints-to="abuse@aioe.org";
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101
Thunderbird/102.4.1
Content-Language: pt-BR, en-GB
X-Notice: Filtered by postfilter v. 0.9.2
 by: castAway - Thu, 10 Nov 2022 22:33 UTC

Hello.

I would like to share my `datediff.sh' script.

The script takes two dates and calculates time intervals between them in various units of time.

The fun is that it uses bash arithmetics and `bc' to perform calculations. If available, `BSD/GNU date' programme is warped to interpret date inputs, however the script works without package `date' if input is ``ISO-8601 format''.

It took me a lot of hard work in the last two or so years to get the calculation to work correctly, but things started working better when I read Dershowitz and Reingold paper/book of Calendrical Calculations.

I would like to highlight datediff.sh can calculate a compound time interval, for example, the interval between some two dates may be `10 years + 2 months + 1 week + 10 hours', or you get single unit intervals, for example, `10.18 years' or `239.59 months' and so forth.

The compound time range code was pretty hard to write, specially what Hroptatyr calls `Refinement Rules'. But also, I was able to implement support for time offset and envar $TZ into the calculation. It can even generate the Unix time stamp and day of the week (RFC-5322) of the dates independently with shell arithmetics!

I hope this script may be useful as it works with bash 2.05b+ versions. The script is really complex but I reckon I have finished developing it (i.e. I don't reckon there is anything else I need it do). It is well tested with millions of dates and the core code seems quite stable, IMHO.

If someone can find any bugs or shed advice for improvement, I would hear it.

The ``datediff.sh'' script is published in GitHub at: github [dot] com/mountaineerbr/scripts

Below is a copy (hopefully formatting is preserved!).
###

#!/usr/bin/env bash
# datediff.sh - Calculate time ranges between dates
# v0.20 nov/2022 mountaineerbr GPLv3+
shopt -s extglob #bash2.05b+

HELP="NAME
${0##*/} - Calculate time ranges/intervals between dates

SYNOPSIS
${0##*/} [-NUM] [-Rrttuvvv] [-f\"FMT\"] \"DATE1\" \"DATE2\" [UNIT]
${0##*/} -[el] [-v] YEAR..
${0##*/} -h

DESCRIPTION
Calculate time intervals between DATE1 and DATE2 or check for leap
years. The \`date' programme is optionally run to process dates.

\`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 available 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.

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 -u sets or prints dates in Coordinated Universal Time (UTC).

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 arithmetics to perform most time range cal-
culations, as long as input is a valid ISO-8601 date format.

Option -d sets \$TZ=UTC, unsets verbose switches and run checks
against \`datediff' and \`date' (dump only when results differ),
set twice to code exit only.

Option -D disables \`date' package warping and -DD disables bash
\`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 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.

Bash2.05b+ is required. \`Bc' is required for single-unit calcula-
tions. FreeBSD12+ or GNU \`date' is optionally required.

Please consider sending me a nickle!
=) bc1qlxm5dfjl58whg6tvtszg5pfna9mn2cr2nulnjr

EXAMPLES
Leap year check
$ ${0##*/} -l 2000
$ ${0##*/} -l {1980..2000}
$ echo 2000 | ${0##*/} -l

#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, see help page text.
-e YEAR Print Easter date for given YEAR.
-f FMT Input time format string (only with BSD \`date').
-h Print this help page.
-l YEAR Check if YEAR is leap year.
-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.
-v Less verbose.
-vv Print only single unit ranges.
-vvv Print only compound range."

#TESTING RESULTS
#!# MAIN TESTING SCRIPT: <https://pastebin.com/suw4Bif3>
# Hroptatyr's `man datediff' says ``refinement rules'' cover over 99% cases.
# Calculated `datediff' error rate is at least .00311 (0.3%) of total tested dates (compound range).
# Results differ from `datediff' in .006275 (0,6%) of all tested dates in script version v0.16.8 (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 `datediff' in compound range are not necessarily errors in all cases and may be considered correct albeit with different refinements. This seem to be the case for most, if not all, differences obtained in testing results.
# No errors were found in range (seconds) calculation, thus single-unit results should all be correct.
#!# OFFSET TESTING SCRIPT: <https://pastebin.com/BvH6PDjC>
# Note `datediff' offset ranges between -14h and +14h.
# All offset-aware date results passed checking against `datediff'.

#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/.:+-'
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'
#`BSD date' input time format defaults:
INPUT_FMT="${TIME_ISO8601_FMT:0:17}" #%Y-%m-%dT%H:%M:%S

# 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.
datefun()
{ local 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
} #test for BSD or GNU date
if DATE_CMD=date; ! date --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

#print the maximum number of days of a given month
#usage: month_maxday [MONTH] [YEAR]
#MONTH range 1-12; YEAR cannot be nought.
month_maxday()
{ local month year
month="$1" year="$2"
if (( month == 2 && !(year % 4) && ( year % 100 || !(year % 400) ) ))
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.
year_days_adj()
{ local month year
month="$1" year="$2"
if (( month <= 2 && !(year % 4) && ( year % 100 || !(year % 400) ) ))
then echo 366
else echo 365
fi
}

#check for leap year
isleap()
{ local year
if ((year=${1//[!+-]}10#${1//[+-]})) ;[[ $year ]]
then
if (( !(year % 4) && ( year % 100 || !(year % 400) ) ))
then ((OPTVERBOSE)) || printf 'leap year -- %04d\n' $year ;return 0
else ((OPTVERBOSE)) || printf 'not leap year -- %04d\n' $year
fi
else echo "err: year must be in the format YYYY" >&2
fi
return $((++RET))
} #https://stackoverflow.com/questions/32196629/my-shell-script-for-checking-leap-year-is-showing-error

#check Easter date in a given year
easterf()
{ echo $(echo ${*} '[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)
}

#datediff fun
mainf()
{ local date1_iso8601 date2_iso8601 unix1 unix2 inputA inputB range neg_range date_buf yearA monthA dayA hourA minA secA tzA neg_tzA tzAh tzAm tzAs yearB monthB dayB hourB minB secB tzB neg_tzB tzBh tzBm tzBs ret 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 date2_month_max_day date3_month_max_day date1_year_days_adj d_left y mo w d h m s bc 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_dow date2_dow u1_dow u2_dow varname var ok ar n p q r v SS SSS TZh TZm TZs TZ_neg TZ_pos

(($# == 1)) && set -- '' "$1"

#warp `date' when available
if unix1=$(datefun "${1:-+%s}" ${1:++%s}) &&
unix2=$(datefun "${2:-+%s}" ${2:++%s})
then {
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'

#sort dates
if ((unix1 > unix2))
then neg_range=-1
pairSwapf unix1 date1_iso8601 date1_iso8601_pr
set -- "$2" "$1" "${@:3}"
fi
else unset unix1 unix2
#set default date -- AD
[[ ! $1 || ! $2 ]] && {
$OPTDD printf -v now "%(${TIME_ISO8601_FMT})T" -1 \
|| now=1970-01-01T00:00:00
}
[[ ! $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="1970-01-01T${inputA}"
[[ $inputB = *([0-9]):* ]] && inputB="1970-01-01T${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 #https://www.oasys.net/fragments/leading-zeros-in-bash/

#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" >&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 ((sec${v}${p}tz+=60 , --min${v}${p}tz))
elif ((sec${v}${p}tz>59))
then ((sec${v}${p}tz%=60 , ++min${v}${p}tz))
fi

#minAtz minBtz minAprtz minBprtz
((min${v}${p}tz+=min${v}-(tz${v}m*neg_tz${v}) ))
if ((min${v}${p}tz<0))
then ((min${v}${p}tz+=60 , --hour${v}${p}tz))
elif ((min${v}${p}tz>59))
then ((min${v}${p}tz%=60 , ++hour${v}${p}tz))
fi

#hourAtz hourBtz hourAprtz hourBprtz
((hour${v}${p}tz+=hour${v}-(tz${v}h*neg_tz${v}) ))
if ((hour${v}${p}tz<0))
then ((hour${v}${p}tz+=24 , --day${v}${p}tz))
elif ((hour${v}${p}tz>23))
then ((hour${v}${p}tz%=24 , ++day${v}${p}tz))
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

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 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
pairSwapf 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
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
(( !(y_test % 4) && (y_test % 100 || !(y_test % 400) ) )) && ((++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) && !(yearB % 4) && (yearB % 100 || !(yearB % 400) ) ))
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) && !(yearA % 4) && (yearA % 100 || !(yearA % 400) ) ))
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 $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 1970-01-01T00:00:00 $var) || ((ret+=$?))

((year${varname}<1970)) && ((var*=-1))
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 ! { $OPTDD printf -v date2_iso8601_pr "%($TIME_RFC5322_FMT)T" $unix2 &&
printf -v date1_iso8601_pr "%($TIME_RFC5322_FMT)T" $unix1 ;}
then #calculate Day Of Week (bash v<3.1)
((u2_dow=unix2-(((TZh*60*60)+(TZm*60)+TZs)*TZ_neg) ))
((u1_dow=unix1-(((TZh*60*60)+(TZm*60)+TZs)*TZ_neg) ))
date2_dow=${DAY_OF_WEEK[(((u2_dow+(u2_dow<0?1:0))/(24*60*60))%7 +(u2_dow<0?6:7))%7]}
date1_dow=${DAY_OF_WEEK[(((u1_dow+(u1_dow<0?1:0))/(24*60*60))%7 +(u1_dow<0?6:7))%7]}
#modulus as (a%b + b)%b to avoid negative remainder.
#<https://www.geeksforgeeks.org/modulus-on-negative-numbers/>
date2_iso8601_pr=$(printf \
'%s, %02d %s %04d %02d:%02d:%02d %s%02d:%02d:%02d\n' \
"${date2_dow: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_dow: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)) &&
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}) * 3600 * 24) ) / (${date1_year_days_adj:-0} * 3600 * 24) ) ) , $SCL); /** YEARS **/
r( (${monthcount:-0} + ( (${range:-0} - (${fullmonth_days_save:-0} * 3600 * 24) ) / (${date1_month_max_day:-0} * 3600 * 24) ) ) , $SCL); /** MONTHS **/
r( (${range:-0} / 604800) , $SCL); /** WEEKS **/
r( (${range:-0} / 86400) , $SCL); /** DAYS **/
r( (${range:-0} / 3600) , $SCL); /** HOURS **/
r( (${range:-0} / 60) , $SCL); /** MINUTES **/") )
#ARRAY: 0=YEARS 1=MONTHS 2=WEEKS 3=DAYS 4=HOURS 5=MINUTES
then #choose layout of single units
if ((OPTT || !OPTLAYOUT))
then #layout one
prHelpf ${OPTTy:+${bc[0]}} && range_pr="${bc[0]} year$SS"
prHelpf ${OPTTmo:+${bc[1]}} && range_pr="$range_pr | ${bc[1]} month$SS"
prHelpf ${OPTTw:+${bc[2]}} && range_pr="$range_pr | ${bc[2]} week$SS"
prHelpf ${OPTTd:+${bc[3]}} && range_pr="$range_pr | ${bc[3]} day$SS"
prHelpf ${OPTTh:+${bc[4]}} && range_pr="$range_pr | ${bc[4]} hour$SS"
prHelpf ${OPTTm:+${bc[5]}} && range_pr="$range_pr | ${bc[5]} min$SS"
prHelpf $range ;((!OPTT||OPTTs)) && range_pr="$range_pr | $range sec$SS"
range_pr="${range_pr# | }" ;((OPTT&&OPTV)) && range_pr="${range_pr% *}"
else #layout two
((n = ${#range}+SCL+1)) #range in seconds is the longest string
prHelpf ${bc[0]} $n && range_pr=Year$SS$'\t'$SSS${bc[0]}
prHelpf ${bc[1]} $n && range_pr="$range_pr"$'\n'Month$SS$'\t'$SSS${bc[1]}
prHelpf ${bc[2]} $n && range_pr="$range_pr"$'\n'Week$SS$'\t'$SSS${bc[2]}
prHelpf ${bc[3]} $n && range_pr="$range_pr"$'\n'Day$SS$'\t'$SSS${bc[3]}
prHelpf ${bc[4]} $n && range_pr="$range_pr"$'\n'Hour$SS$'\t'$SSS${bc[4]}
prHelpf ${bc[5]} $n && range_pr="$range_pr"$'\n'Min$SS$'\t'$SSS${bc[5]}
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
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
((OPTVERBOSE<2 || OPTVERBOSE>2)) && printf '%dY %02dM %02dW %02dD %02dh %02dm %02ds\n' "${sh[@]}"
((OPTVERBOSE<3)) && printf '%s\n' "${range_pr:-$range secs}"

return ${ret:-0}
}

#execute result checks against `datediff' and `date'
debugf()
{ local 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
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[*]}}" ]]
} || { echo -ne "\033[2K" >&2
echo "\
sh=${sh[*]} dd=${dd[*]} | "\
"$iA $iB | "\
"${range:-unavail} ${range_check:-unavail} | "\
"${date1_iso8601_pr:0:25} $checkA_pr | "\
"${date2_iso8601_pr:0:25} $checkB_pr | "\
"${date1_iso8601_pr:0:3} ${checkA_pr_dow:0:3} | "\
"${date2_iso8601_pr:0:3} ${checkB_pr_dow:0:3} | "\
"$tB $checkB_utc | "\
"$tA $checkA_utc | "\
"${date_cmd_save%date}"

((ret+=1))
}

#((DEBUG>1)) && return ${ret:-0} #!#
((DEBUG>1)) && exit ${ret:-0} #!#
return 0
}

#swap $varA/$varB or $var1/$var2 values
pairSwapf()
{ local varname buf p q
for varname
do [[ $varname = *A* ]] && p=A q=B || p=1 q=2
eval "buf=\"\$$varname\""
eval "$varname=\"\$${varname/$p/$q}\" ${varname/$p/$q}=\"\$buf\""
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]
prHelpf()
{ local 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
}

## Parse options
while getopts 01234567890Ddef:hlRr@Vtuv opt
do case $opt in
[0-9]) SCL="$SCL$opt"
;;
d) ((++DEBUG))
;;
D) [[ $DATE_CMD = false ]] && OPTDD=false ;DATE_CMD=false
;;
e) OPTE=1 OPTL=
;;
f) INPUT_FMT="$OPTARG" OPTF=1 #input format string for `BSD date'
;;
h) while read
do [[ "$REPLY" = \#\ v* ]] && echo "$REPLY" && break
done <"$0"
echo "$HELP" ;exit
;;
l) OPTL=1 OPTE=
;;
R) OPTRR=1
;;
r|@) OPTR=1
;;
t|V) ((++OPTLAYOUT)) #option -V is deprecated
;;
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

#stdin input
[[ ${1//[$IFS]} = $GLOBOPT ]] && opt="$1" && shift
if [[ $# -eq 0 && ! -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[@]} -eq 3 || ${#ar[@]} -eq 2) && \ $REPLY = @(*[$IFS]$GLOBOPT*|$globtest) ]]
then set -- $REPLY ;[[ $1 = $GLOBOPT ]] || break
fi
else if [[ ${#ar[@]} -eq 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"

#print single time unit?
opt="${opt:-${@: -1}}" opt="${opt//[$IFS]}"
if [[ $opt = $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.
[[ $1 = [a-zA-Z] || $2 = [a-zA-Z] ]] && { echo "err: illegal user input" >&2 ;exit 2 ;}

#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

#-r, unix times
if ((OPTR && $#>1))
then set -- @"${1#@}" @"${2#@}" "${@:3}"
elif ((OPTR && $#))
then set -- @"${1#@}"
fi

if ((OPTL || OPTE))
then [[ $* ]] || set -- $($OPTDD printf '%(%Y)T' -1) || set -- 1970
for year in $*
do if ((OPTL))
then isleap $year
else easterf $year
fi
done
else mainf "$@" #datediff fun
fi

###
Cheers!
JSN

SubjectRepliesAuthor
o Datediff script

By: castAway on Thu, 10 Nov 2022

57castAway
server_pubkey.txt

rocksolid light 0.9.81
clearnet tor