Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

Datediff script

53 views
Skip to first unread message

castAway

unread,
Nov 10, 2022, 5:33:36 PM11/10/22
to
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

Janis Papanagnou

unread,
Nov 10, 2022, 7:47:02 PM11/10/22
to
On 10.11.2022 23:33, castAway wrote:
> Hello.
>
> I would like to share my `datediff.sh' script.

(Note that it's rather a datediff.bash script as presented.)

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

What does the script that AT&T date or GNU date doesn't support in this
(time-intervals) respect?

Since I notice some mention of "easter"; which definitions of "easter"
does it support? (I presume you know that there's not a single one.)

Does your script support moon phases? (Something I recently looked for,
so I am curious.)

>
> 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''.

I don't get it. What's the point in using bash arithmetics and bc (or
only sometimes)? I mean it wouldn't occur to me that multi-precision
arithmetic would be necessary for that. For floating point arithmetic,
OTOH, I'd use ksh (instead of bash) and generally omit bc. (I just
noticed that you additionally use dc as well; the dc-code used in
easterf() looks horrible, for my taste, and quite unmaintainable as
presented.)

>
> 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.

Looks to me like the mentioned date programs or a simple GNU awk date
implementation supports that already.

>
> 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 used inherent TZ support already with dates used in scripts that use
GNU awk dates and in AT&T or ksh dates. Why do you mention TZ support
as something special here?

>
> 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.

You could have avoided unnecessary bash specifics to make your script
run on other prominent shells (ksh, zsh, or even sh) as well. (Some
changes are trivial, like local in functions.)

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

I just skimmed through it and saw some strange constructs and comments;
e.g. "YYY-..." in a comment named ISO-date, or a $((++RET)) return value
with undefined and otherwise unused RET variable. (I know it works but
implementing side effects on undefined global scoped variables that
will change the return/exit code of a function cal every time, and will
at times overflow the allowed return value range is not something that
looks trustworthy to me.) There's a couple places where you use obsolete
constructs, mix legacy (-eq) and new syntax, or use unnecessary complex
expressions (e.g. (( (yearA||yearA==0) ... )) in arithmetic context).
There's probably a lot more to find given the huge size of that script.

Note that I've written my questions and comments because the script is
so large, partly confusing, and not that confidence-inspiring [to me].
Must have been a pain to write and debug that large piece of code. So
please don't take my hints and comments too serious. I'm just critical
and skeptical if I see such code. And you asked for it.

Janis

>
> The ``datediff.sh'' script is published in GitHub at: github [dot]
> com/mountaineerbr/scripts
>
> Below is a copy (hopefully formatting is preserved!).
> ###
[ snip 1000+ lines of code ]
> ###
> Cheers!
> JSN

castAway

unread,
Nov 10, 2022, 10:34:53 PM11/10/22
to
On 11/10/22 21:46, Janis Papanagnou wrote:
> What does the script that AT&T date or GNU date doesn't support in this
> (time-intervals) respect?

I am not sure that `awk' or `date' can be used to calculate time intervals
other than in days, hours, minutes and seconds. So how can you calculate
these time differences with awk? You could get to an _approximate_ result
very easily but it gets more difficult as we need to compensate for a lot
of things, for example not all months have 4 weeks.

> Since I notice some mention of "easter"; which definitions of "easter"
> does it support? (I presume you know that there's not a single one.)
Oh, I just got that Easter script from Dershowitz and Reingold book and
they seem to agree with you about the style of it:

[[ Finally, the computer world is plagued with unintelligible code that seems to work
by magic. Consider the following Unix script for calculating the date of Easter:

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

We want to provide transparent algorithms to replace the gobbledegook that is so
common. ]]

Dc is used only in this function and really I haven't changed it. Also, I am not
sure what you mean by Easter definition?

> Does your script support moon phases? (Something I recently looked for,
> so I am curious.)

The script does not support moon phases. I just checked Dershowitz and Reingold
book and there is even some Lisp code examples. It seems that to find out when
new moon is, you need first calculate moon and sun latitudes, and at least
Venus and Jupiter influence seem worth correcting for... So that is rather
a complex astronomical calculation!

> I don't get it. What's the point in using bash arithmetics and bc (or
> only sometimes)? I mean it wouldn't occur to me that multi-precision
> arithmetic would be necessary for that. For floating point arithmetic,
> OTOH, I'd use ksh (instead of bash) and generally omit bc.

I use bc because you may want to know how many weeks are 10 days? Then
it needs to have decimal precision. I would use Ksh preferably, or Zsh,
however I had to choose one shell and it was bash because I reckon it
is installed in most systems.

> Looks to me like the mentioned date programs or a simple GNU awk date
> implementation supports that already.

I have written some weird scripts that only later did I found out had
their functions already implemented in very well established C programmes.
If that is the case, I would like to know how to do it, because all
datediff shell scripts I have seem can only calculate up to the time unit
of days with precision. But I may be wrong.

> I used inherent TZ support already with dates used in scripts that use
> GNU awk dates and in AT&T or ksh dates. Why do you mention TZ support
> as something special here?
$TZ is mentioned because the script is aware of it even if no date package
is available, thus, it subtracts $TZ from both input dates. $TZ mostly
influences how the processed dates are printed and it is used to find
the UTC time. All calculations are made using UTC times internally.

> You could have avoided unnecessary bash specifics to make your script
> run on other prominent shells (ksh, zsh, or even sh) as well. (Some
> changes are trivial, like local in functions.)

I decided to use all the features of bash to make the script run as fast
as it possibly can. Using test [ instead of [[ makes the script runs
slower, using awk or sed for trivial things are also unnecessary.

> I just skimmed through it and saw some strange constructs and comments;
> e.g. "YYY-..." in a comment named ISO-date, or a $((++RET)) return value
> with undefined and otherwise unused RET variable. (I know it works but
> implementing side effects on undefined global scoped variables that
> will change the return/exit code of a function cal every time, and will
> at times overflow the allowed return value range is not something that
> looks trustworthy to me.)

That is a good point, that variable use is a little weird. I thought to exit
with the code of the same number of failed tests (that is used only in an
isolated function to check if some year is leap or not, so if input is
three years and two of them are not leap years, then the function exits with
2. I thought this may be more useful than exiting with only 1, and yes, it
will overflow with more than 255 increments and may exit with 0.
So this logic is problematic because it is not explained in the help page
and is unstable. In this sense, I will change it to exit with 1 if any
input years is not leap.

> There's a couple places where you use obsolete
> constructs, mix legacy (-eq) and new syntax, or use unnecessary complex
> expressions (e.g. (( (yearA||yearA==0) ... )) in arithmetic context).

These are great observations. I would elaborate on why I used legacy syntax
[[-eq]] but I see your point and will change those instances with ((==)) tests.

Now I only disagree on unnecessary complex testing you pointed out!
In the expression "(( (yearA||yearA==0) ))" I do two tests, the first one
will catch in case input is something not numerical, thus is $yearA is
some weird string, it will fail this test, but there is still the case when
year may be 0000 and thus we also check if that is the case.

I am very interested in simplifying calculations, if you could point out
the unnecessary complexity, I will take a look on that.

> Must have been a pain to write and debug that large piece of code. So
> please don't take my hints and comments too serious. I'm just critical
> and skeptical if I see such code. And you asked for it.

It was a pain to debug it, specially because I am no developer and not very
good with mathematics. But I cannot see how the code could be less robust.

Hope the code can be minimally verified here so I can get more confident.
However, Hroptatyr, who is the developer of C programme `datediff', didn't
say anything bad about my script, in fact he starred it, so IDK.


Spiros Bousbouras

unread,
Nov 11, 2022, 5:38:17 AM11/11/22
to
On Fri, 11 Nov 2022 00:34:37 -0300
castAway <n...@where.com> wrote:
> On 11/10/22 21:46, Janis Papanagnou wrote:
> > Since I notice some mention of "easter"; which definitions of "easter"
> > does it support? (I presume you know that there's not a single one.)
> Oh, I just got that Easter script from Dershowitz and Reingold book and
> they seem to agree with you about the style of it:
>
> [[ Finally, the computer world is plagued with unintelligible code that seems to work
> by magic. Consider the following Unix script for calculating the date of Easter:

[...]

> Dc is used only in this function and really I haven't changed it. Also, I am not
> sure what you mean by Easter definition?

Western Easter is generally on a different date than Eastern Orthodox Easter.
For example the ncal man page mentions
-e Display date of easter (for western churches).
[...]
-o Display date of orthodox easter (Greek and Russian Orthodox Churches).

castAway

unread,
Nov 11, 2022, 10:38:20 AM11/11/22
to
On 11/11/22 07:38, Spiros Bousbouras wrote:
> Western Easter is generally on a different date than Eastern Orthodox Easter.
> For example the ncal man page mentions
> -e Display date of easter (for western churches).
> [...]
> -o Display date of orthodox easter (Greek and Russian Orthodox Churches).

Thanks for that definition. I downloaded ncal from bsdmainutils and checked it.

The unix function from Dershowitz and Reingold book displays Western Church Easter!

I implemented what Papanagnou & Bousbouras suggested and those modifications
are already available at my GitHub. Thanks.

Janis Papanagnou

unread,
Nov 11, 2022, 11:36:52 AM11/11/22
to
On 11.11.2022 04:34, castAway wrote:
> On 11/10/22 21:46, Janis Papanagnou wrote:
>> What does the script that AT&T date or GNU date doesn't support in this
>> (time-intervals) respect?
>
> I am not sure that `awk' or `date' can be used to calculate time intervals
> other than in days, hours, minutes and seconds. So how can you calculate
> these time differences with awk? You could get to an _approximate_ result
> very easily but it gets more difficult as we need to compensate for a lot
> of things, for example not all months have 4 weeks.

If I'd implement time and date calculations I'd separate the tasks in
three major components; a) transform various external representations
into an internal format, b) do some date and time calculations based
on a tool-chest of basic functions, c) provide output for any desired
external representation. - I understood that in step b you're doing a
difference on dates? - So I'd take the time/date-features that GNU
supports or the native features of tools that already support dates
(date, ksh, etc.), do the transformation, calculation, and output.
(But frankly, I may have misunderstood what your tool actually does.)

>
>> Since I notice some mention of "easter"; which definitions of "easter"
>> does it support? (I presume you know that there's not a single one.)
> Oh, I just got that Easter script from Dershowitz and Reingold book and
> they seem to agree with you about the style of it:
>
> [[ Finally, the computer world is plagued with unintelligible code that
> seems to work
> by magic. Consider the following Unix script for calculating the date of
> Easter:
>
> 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
>
> We want to provide transparent algorithms to replace the gobbledegook
> that is so
> common. ]]
>
> Dc is used only in this function and really I haven't changed it.

The point was not so much that you took some cryptic code and just
trusted it, it was that if you want to verify that code - in case
of errors or to anticipate any potential errors - you should very
well understand it or be enabled to do so. Neither the cryptic dc
code nor the absence of a comment or [specific] reference supports
that. In other words, the more unclear the construct is the more
inline documentation for support should be present. (Or other more
obvious and comprehensible solutions used.)

> Also, I am not sure what you mean by Easter definition?

Depending on the concrete religion Easter will fall on different
dates (and it's not just a constant offset; it varies from year
to year). I'm not too familiar with the details of any religion
but, e.g., in Greece (orthodox church) Easter is celebrated on a
different date than in Germany (e.g. catholic church).

>
>> Does your script support moon phases? (Something I recently looked for,
>> so I am curious.)
>
> The script does not support moon phases. I just checked Dershowitz and
> Reingold
> book and there is even some Lisp code examples. It seems that to find
> out when
> new moon is, you need first calculate moon and sun latitudes, and at least
> Venus and Jupiter influence seem worth correcting for... So that is rather
> a complex astronomical calculation!

I was recently looking at some code and there's probably similar
cryptic formulas existing. I used an adapted _quantized_ version
(only 8 phases, and a plain integer calculation) for a web page:

function phase_of_the_moon (now) // 0-7, with 0: new, 4: full
{
var diy = day_in_year (now);
var goldn = (now.getFullYear() % 19) + 1;
var epact = (11 * goldn + 18) % 30;
if ((epact == 25 && goldn > 11) || epact == 24)
epact++;
return Math.floor (((((diy + epact) * 6) + 11) % 177) / 22) % 8;
}

Not too complex (but also without comments, so I cannot tell,
to be honest, what's actually going on). I suppose it could be
simplified for an accurate FP representation. (I think to have
seen such a function also on the Net, maybe even on Wikipedia.)

>
>> I don't get it. What's the point in using bash arithmetics and bc (or
>> only sometimes)? I mean it wouldn't occur to me that multi-precision
>> arithmetic would be necessary for that. For floating point arithmetic,
>> OTOH, I'd use ksh (instead of bash) and generally omit bc.
>
> I use bc because you may want to know how many weeks are 10 days? Then
> it needs to have decimal precision. I would use Ksh preferably, or Zsh,
> however I had to choose one shell and it was bash because I reckon it
> is installed in most systems.

Well, I understand your motivation now, but I think that it's not
true, unfortunately. In commercial Unix environments you cannot
rely on the availability of a bash. (That's my experience from
a couple commercial companies I worked for.) Availability of ksh
was different in that respect; I found it everywhere.

But okay, your choice.

>
>> Looks to me like the mentioned date programs or a simple GNU awk date
>> implementation supports that already.
>
> I have written some weird scripts that only later did I found out had
> their functions already implemented in very well established C programmes.
> If that is the case, I would like to know how to do it, because all
> datediff shell scripts I have seem can only calculate up to the time unit
> of days with precision. But I may be wrong.

I'm not sure to understand what you wrote here. Usually it's standard
to support precisions of seconds at least, often milliseconds or even
microseconds or manoseconds. Of course if the shell doesn't support
sub-seconds you have natively only seconds supported. Here are two
examples

$ awk 'BEGIN{a=strftime("%s");
system("sleep 3");
print strftime("%s")-a}'
3

$ ksh -c 'typeset -F a=$(printf "%(%s.%N)T");
sleep 3.14;
typeset -F b=$(printf "%(%s.%N)T");
printf "%f\n" $((b-a))'
3.141979

>
>> I used inherent TZ support already with dates used in scripts that use
>> GNU awk dates and in AT&T or ksh dates. Why do you mention TZ support
>> as something special here?
> $TZ is mentioned because the script is aware of it even if no date package
> is available, thus, it subtracts $TZ from both input dates. $TZ mostly
> influences how the processed dates are printed and it is used to find
> the UTC time. All calculations are made using UTC times internally.
>
>> You could have avoided unnecessary bash specifics to make your script
>> run on other prominent shells (ksh, zsh, or even sh) as well. (Some
>> changes are trivial, like local in functions.)
>
> I decided to use all the features of bash to make the script run as fast
> as it possibly can. Using test [ instead of [[ makes the script runs
> slower, using awk or sed for trivial things are also unnecessary.

If you were going for speed you'd have better avoided bash. :-)

[[...]] is understood by the prominent shells (ksh, zsh); ksh actually
invented that. But I rather mentioned the (unnecessary) 'local' keyword
that other shells not necessarily support (ksh has 'typeset' for that).
But all shells, even Bourne shell support instead of f(){ local i=0 ;}
the standard f()( i=0 )
What I wanted to express is that with a minimum of change you can widen
applicability to more shells.
As said, I just skimmed through the code because it appears to me too
overloaded, and I don't want to spend more time on that. The sad fact
is that when I start I go into every even non-functional detail, say,
code like
TIME_ISO8601_FMT='%Y-%m-%dT%H:%M:%S%z'
...
INPUT_FMT="${TIME_ISO8601_FMT:0:17}" #%Y-%m-%dT%H:%M:%S
where you use a non-standard variable expansion unnecessarily and then
commenting the actual value.
Or that you seem to have a lot duplicate code (e.g. there's a leap-year
function but an explicit leap-year calculation can be found on several
other places as well.
(I better stop now.)

>
>> Must have been a pain to write and debug that large piece of code. So
>> please don't take my hints and comments too serious. I'm just critical
>> and skeptical if I see such code. And you asked for it.
>
> It was a pain to debug it, specially because I am no developer and not very
> good with mathematics. But I cannot see how the code could be less robust.

I think this is a key observation; the debug pain. Try to make small
well defined and easy to understand and test code pieces; reuse them,
document them, prefer clear and simple to cryptic constructs. Document
specifically and (seemingly) unavoidable complex/cryptic stuff. If I'd
have to put that code into production I'd re-factor it first to make
it easier manageable.

>
> Hope the code can be minimally verified here so I can get more confident.
> However, Hroptatyr, who is the developer of C programme `datediff', didn't
> say anything bad about my script, in fact he starred it, so IDK.

My experience is that in practical computer science there's countless
experts and the blogs and Web is full of dubious statements and code.
There's also some excellent sources, though. I don't know that guy or
his C-code and I see only the shell source code presented here. Based
on one's own expertise folks have to judge themselves whether it fits
them.

Janis

Janis Papanagnou

unread,
Nov 11, 2022, 12:04:19 PM11/11/22
to
On 11.11.2022 17:36, Janis Papanagnou wrote:
> On 11.11.2022 04:34, castAway wrote:
>> On 11/10/22 21:46, Janis Papanagnou wrote:
>>> What does the script that AT&T date or GNU date doesn't support in this
>>> (time-intervals) respect?
>>
>> I am not sure that `awk' or `date' can be used to calculate time intervals
>> other than in days, hours, minutes and seconds. So how can you calculate
>> these time differences with awk? You could get to an _approximate_ result
>> very easily but it gets more difficult as we need to compensate for a lot
>> of things, for example not all months have 4 weeks.
>
> [...]

On re-reading it occurred to me that you were probably not focusing
on the sub-second issue but on support for dates before, say, 1900 or
after 2100, or so. - Yes, Unix tools have different (often restricted)
support for dates outside the Unix Epoch. - So, yes, depending on the
used tool you'd have to transform the external form to an internal
representation that fits this accuracy.

Janis

Janis Papanagnou

unread,
Nov 11, 2022, 12:06:48 PM11/11/22
to
And a final remark... - In this case you have to also consider calendar
switches (Julian/Gregorian) in your date calculations. (Not sure your
code supports that.)

Janis

castAway

unread,
Nov 11, 2022, 2:15:12 PM11/11/22
to
On 11/11/22 13:36, Janis Papanagnou wrote:
> If I'd implement time and date calculations I'd separate the tasks in
> three major components; a) transform various external representations
> into an internal format, b) do some date and time calculations based
> on a tool-chest of basic functions, c) provide output for any desired
> external representation. - I understood that in step b you're doing a
> difference on dates? - So I'd take the time/date-features that GNU
> supports or the native features of tools that already support dates
> (date, ksh, etc.), do the transformation, calculation, and output.
> (But frankly, I may have misunderstood what your tool actually does.)

Thanks for all this advice. I would hear whatever you guys have to say
in comp.unix.shell about style and how to solve coding problems. In step
b, the script counts how many seconds there are between two dates. For that,
it has to loop through all full years (cannot just subtract two dates!).
The remaining full months are also counted one by one. Days that are still
left, can be added to complete one month or a few weeks. Deciding if x days
should add up to a month is tricky as it depends on what month of the year
the first date is.

> The point was not so much that you took some cryptic code and just
> trusted it, it was that if you want to verify that code - in case
> of errors or to anticipate any potential errors - you should very
> well understand it or be enabled to do so. Neither the cryptic dc
> code nor the absence of a comment or [specific] reference supports
> that. In other words, the more unclear the construct is the more
> inline documentation for support should be present. (Or other more
> obvious and comprehensible solutions used.)

That was a good point. This sense of balance between code comment and
code clarity is excellent!

> I was recently looking at some code and there's probably similar
> cryptic formulas existing. I used an adapted _quantized_ version
> (only 8 phases, and a plain integer calculation) for a web page:
>
> function phase_of_the_moon (now) // 0-7, with 0: new, 4: full
> {
> var diy = day_in_year (now);
> var goldn = (now.getFullYear() % 19) + 1;
> var epact = (11 * goldn + 18) % 30;
> if ((epact == 25 && goldn > 11) || epact == 24)
> epact++;
> return Math.floor (((((diy + epact) * 6) + 11) % 177) / 22) % 8;
> }
>

You convinced me that it may be possible to have a moon phase function
that is reasonable to implement within a certain error for the [near]
present time... I *shall* look into this code further and may implement
it at some point! I am not sure about ``a FP representation of it'' yet,
but I will post any follow-ups when I am able to dig into this study..

>>> You could have avoided unnecessary bash specifics to make your script
>>> run on other prominent shells (ksh, zsh, or even sh) as well. (Some
>>> changes are trivial, like local in functions.)

Now that the calculation works, it should not be too hard to implement it
in sh. Maybe if I have got some spare time in the future, that would be
a godo exercise to widen script applicability to other shells, however,
the scripts relies on extended globbing to check and test input...

> As said, I just skimmed through the code because it appears to me too
> overloaded, and I don't want to spend more time on that. The sad fact
> is that when I start I go into every even non-functional detail, say,
> code like
> TIME_ISO8601_FMT='%Y-%m-%dT%H:%M:%S%z'
> ...
> INPUT_FMT="${TIME_ISO8601_FMT:0:17}" #%Y-%m-%dT%H:%M:%S
> where you use a non-standard variable expansion unnecessarily and then
> commenting the actual value.
> Or that you seem to have a lot duplicate code (e.g. there's a leap-year
> function but an explicit leap-year calculation can be found on several
> other places as well.
> (I better stop now.)

Your tidbits on style are quite informative already and I appreciate them.
For this specific example, I reckon it could be written standardly as
INPUT_FMT="${TIME_ISO8601_FMT%??}"
I also see your point on this variable definition not being necessary,
it can indeed be removed and the original variable used instead in this case.

The reason why one can find lots of seemingly duplicate code is either the
code is subtly different and would become more difficult to understand if
these differences were to be set on run time, or simply because of speed
constraints. Millions of dates were tested on my intel i7 and amd processors
and each testing run could take a couple of days.. So the fewest
functions were declared. The leap year checking code is so trivial and easy
to check that is not worth bothering defining a function to it.
Function nesting makes the script run slower.

The organisation point is very important, however the script code is not
a mess as it is, either.

Janis Papanagnou

unread,
Nov 11, 2022, 6:27:37 PM11/11/22
to
On 11.11.2022 20:15, castAway wrote:
> On 11/11/22 13:36, Janis Papanagnou wrote:
>
>> I was recently looking at some code and there's probably similar
>> cryptic formulas existing. I used an adapted _quantized_ version
>> (only 8 phases, and a plain integer calculation) for a web page:
>> [moon phase]
>
> You convinced me that it may be possible to have a moon phase function
> that is reasonable to implement within a certain error for the [near]
> present time... I *shall* look into this code further and may implement
> it at some point! I am not sure about ``a FP representation of it'' yet,
> but I will post any follow-ups when I am able to dig into this study..

(Please note that it was *not* a "Request for Feature". Feel free
to ignore it. There's so many things a time/date library could or
should support.)

>
>>>> You could have avoided unnecessary bash specifics to make your script
>>>> run on other prominent shells (ksh, zsh, or even sh) as well. (Some
>>>> changes are trivial, like local in functions.)
>
> Now that the calculation works, it should not be too hard to implement it
> in sh. Maybe if I have got some spare time in the future, that would be
> a godo exercise to widen script applicability to other shells, however,
> the scripts relies on extended globbing to check and test input...

Ksh supports that (ksh globbing, "extended" globbing) as standard
(i.e. without explicitly activating it) and Zsh as well

>
>> [...]
>> TIME_ISO8601_FMT='%Y-%m-%dT%H:%M:%S%z'
>> ...
>> INPUT_FMT="${TIME_ISO8601_FMT:0:17}" #%Y-%m-%dT%H:%M:%S
>> where you use a non-standard variable expansion unnecessarily and then
>> commenting the actual value.
>> Or that you seem to have a lot duplicate code (e.g. there's a leap-year
>> function but an explicit leap-year calculation can be found on several
>> other places as well.
>> (I better stop now.)
>
> Your tidbits on style are quite informative already and I appreciate them.
> For this specific example, I reckon it could be written standardly as
> INPUT_FMT="${TIME_ISO8601_FMT%??}"

My point was rather to omit an unnecessary operation (both substring
expansion or suffix expansion) by, e.g.,
INPUT_FMT="%Y-%m-%dT%H:%M:%S"
TIME_ISO8601_FMT="${INPUT_FMT}%z"
but preferences vary. Also "${TIME_ISO8601_FMT%??}" is misleading for
several reasons (there is not "one format") and might be interpreted,
e.g., as removing the last seconds-formatter; you'd need to inspect
more context. (As said, a matter of preference, style, or experience
what is more or less robust, more or less portable, etc.)

>
> The reason why one can find lots of seemingly duplicate code is either the
> code is subtly different and would become more difficult to understand if

if (( !(year % 4) && ( year % 100 || !(year % 400) ) )) # your leap fun

if (( month == 2 && !(year % 4) && ( year % 100 || !(year % 400) ) ))
if (( month <= 2 && !(year % 4) && ( year % 100 || !(year % 400) ) ))
(( !(y_test % 4) && (y_test % 100 || !(y_test % 400) ) )) && ((++leapcount))
if (( (month_test == 2) && !(yearB % 4) && (yearB % 100 || !(yearB %
400) ) ))
if (( (month_test == 2) && !(yearA % 4) && (yearA % 100 || !(yearA %
400) ) ))

vs.

if (( !(year % 4) && ( year % 100 || !(year % 400) ) )) # your leap fun

if (( month == 2 )) && is_leapyear "${year}"
if (( month <= 2 )) && is_leapyear "${year}"
is_leapyear "${y_test}" && ((++leapcount))
if (( month_test == 2 )) && is_leapyear "${year_B}"
if (( month_test == 2 )) && is_leapyear "${year_A}"

Maintainability aside; are you really saying the code using a function
call is "more difficult to understand"?

And where is the "subtle difference"? - And why is any subtle difference
easier to detect in unstructured copy/past code? - I'll bite!

> these differences were to be set on run time, or simply because of speed
> constraints. Millions of dates were tested on my intel i7 and amd
> processors
> and each testing run could take a couple of days..

The leapyear test code runs on my old legacy system for 1 million tests in
20 seconds (bash, expanded expr)
38 seconds (bash, function call)
3.3 seconds (ksh, expanded expr)
8.0 seconds (ksh, function call)
3 seconds (zsh, expanded expr)
12 seconds (zsh, function call)

> So the fewest
> functions were declared. The leap year checking code is so trivial and easy
> to check that is not worth bothering defining a function to it.
> Function nesting makes the script run slower.

If you have a speed issue, again, I suggest to switch shells for a try.
Bash is slow (see above).

But using a slow shell and then sacrificing maintainability because of
that is even more an issue. YMMV.

In the concrete cases I'd certainly prefer
if is_leapyear "${year}" && optional_other_code
then ...
in the places where you have a duplicated leap-year test expression.

This quality of coding is (at least for me) a criterion to use or ignore
any piece of code. (Or rewrite it.)

>
> The organisation point is very important, however the script code is not
> a mess as it is, either.

No? - If you say so then I take your word for granted. ;-) Have fun!

Janis

Chris Elvidge

unread,
Nov 12, 2022, 7:07:24 AM11/12/22
to
Alternative(ish) to this script; have you looked at dateutils?
http://www.fresse.org/dateutils/



--
Chris Elvidge
England

Spiros Bousbouras

unread,
Nov 12, 2022, 8:33:53 AM11/12/22
to
On Sat, 12 Nov 2022 12:07:15 +0000
Chris Elvidge <ch...@mshome.net> wrote:
> On 11/11/2022 23:27, Janis Papanagnou wrote:
> > On 11.11.2022 20:15, castAway wrote:

[...]

> Alternative(ish) to this script; have you looked at dateutils?
> http://www.fresse.org/dateutils/

Assuming "you" is castAway , he mentioned Hroptatyr in his opening post (and
later) and www.fresse.org has "profile for hroptatyr on Stack Exchange" .
So the answer is probably yes.

castAway

unread,
Nov 12, 2022, 10:40:24 AM11/12/22
to
On 11/10/22 21:46, Janis Papanagnou wrote:
> Looks to me like the mentioned date programs or a simple GNU awk date
> implementation supports that already.

I decided to try and change the script to fit Ksh. More on that later..

Now, I just noticed that `AST date' has got an -E (elapsed) flag:

-E, --elapsed Interpret pairs of arguments as start and stop dates, sum the
differences between all pairs, and list the result as a
fmtelapsed(3) elapsed time on the standard output. If there
are an odd number of arguments then the last time argument is
differenced with the current time.

However, the functionality seems to be very basic:

% $AST/date -E '2002-01-01' '2012-01-01'
9Y11M
% $AST/date -E '12:01:01' '19:02:02'
7h01m
% $AST/date -E '2002-01-01 12:01:01' '2012-01-01 19:01:01'
9Y11M


Now, I know that KSH is very fast and I also read that Ksh code is very convoluted
or very difficult to read and maintain, for example, Mr Siteshwar and Mr Rader
complained a lot of the Ksh code when they were at it trying to develop it further.

So, it comes to me that Mr Korn sacrificed readability and maintainability in
exchange for speed, or am I wrong?

Cheers,
JSN

castAway

unread,
Nov 13, 2022, 8:15:54 AM11/13/22
to
On 11/11/22 13:36, Janis Papanagnou wrote:
> I was recently looking at some code and there's probably similar
> cryptic formulas existing. I used an adapted _quantized_ version
> (only 8 phases, and a plain integer calculation) for a web page:
>
> function phase_of_the_moon (now) // 0-7, with 0: new, 4: full
> {
> var diy = day_in_year (now);
> var goldn = (now.getFullYear() % 19) + 1;
> var epact = (11 * goldn + 18) % 30;
> if ((epact == 25 && goldn > 11) || epact == 24)
> epact++;
> return Math.floor (((((diy + epact) * 6) + 11) % 177) / 22) % 8;
> }
>
> Not too complex (but also without comments, so I cannot tell,
> to be honest, what's actually going on). I suppose it could be
> simplified for an accurate FP representation. (I think to have
> seen such a function also on the Net, maybe even on Wikipedia.)

What about the following script to get phase of the moon in Ksh?


YEAR_MONTH_DAYS=(31 28 31 30 31 30 31 31 30 31 30 31)
function is_leapyear
{
((!($1 % 4) && ($1 % 100 || !($1 % 400) ) ))
}
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
}


#day in the year
function diyf
{
typeset day month year month_test daysum
day="$1" month="$2" year="$3"
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
function phase_of_the_moon
{
typeset day month year diy goldn epact
day="$1" month="$2" year="$3"

diy=$(diyf "$day" "$month" "$year")
((goldn = (year % 19) + 1))
((epact = (11 * goldn + 18) % 30))
(((epact == 25 && goldn > 11) || epact == 24 )) && ((++epact))

case $(( floor((((((diy + epact) * 6) + 11) % 177) / 22) % 8) )) in
0) echo 'New Moon' ;;
1) echo 'Waxing Crescent' ;;
2) echo 'First Quarter' ;;
3) echo 'Waxing Gibbous' ;;
4) echo 'Full Moon' ;;
5) echo 'Waning Gibbous' ;;
6) echo 'Last Quarter' ;;
7) echo 'Waning Crescent' ;;
esac
}


% phase_of_the_moon 13 11 2022
Waning Gibbous
% phase_of_moon 1 1 1970
Last Quarter


I tentatively checked results against data from
<https://aa.usno.navy.mil/data/MoonPhases>
<http://astropixels.com/ephemeris/phasescat/phases1901.html>
The function prints:
(day, month and year) -> Lunar Phase
"29 12 1969" -> "Waning Gibbous"
"30 12 1969" -> "Last Quarter" [according to usno.navy could still be _Waning Gibbous_]
"31 12 1969" -> "Last Quarter"
However, data from those websites don't match exactly, in the previous example, "Last Quarter" should start only on 31/Dec/2022.
Also:
"24 12 1700" -> "Waxing Gibbous"
"25 12 1700" -> "Full Moon" [according to usno.navy could still be _Waxing Gibbous_]
"26 12 1700" -> "Full Moon"
and:
"25 12 2021" -> "Last Quarter"
"26 12 2021" -> "Last Quarter"
"27 12 2021" -> "Last Quarter" [according to usno.navy _Last Quarter_ should start at this day]
"28 12 2021" -> "Waning Crescent"

Data from these websites have got only 4 lunar phases "New Moon, First Quarter, Full Moon, Last Quarter", while the function returns results within 8 lunar phases, so maybe the reading-frame is a little more relaxed?

I shall do some more research (maybe try and compare some code, there are 777 results from GitHub for "Moon Phase")..

Cheers,
JSN


castAway

unread,
Nov 13, 2022, 8:19:45 AM11/13/22
to
On 11/12/22 09:07, Chris Elvidge wrote:
> Alternative(ish) to this script; have you looked at dateutils? http://www.fresse.org/dateutils/
>

Yes, I am aware of Dateutils. By the results I got from testing,
the datediff.sh script is just as good as dateutils' datediff programme.
In fact, I found some weird results from Dateutils datediff that I could
not find with datediff.sh. However, Dateutils datediff is much more robust.

castAway

unread,
Nov 13, 2022, 3:23:42 PM11/13/22
to
On 11/13/22 10:15, castAway wrote:
> On 11/11/22 13:36, Janis Papanagnou wrote:
>> I was recently looking at some code and there's probably similar
>> cryptic formulas existing. I used an adapted _quantized_ version
>> (only 8 phases, and a plain integer calculation) for a web page:
>>
>>    function phase_of_the_moon (now)    // 0-7, with 0: new, 4: full
>>    {
>>      var diy = day_in_year (now);
>>      var goldn = (now.getFullYear() % 19) + 1;
>>      var epact = (11 * goldn + 18) % 30;
>>      if ((epact == 25 && goldn > 11) || epact == 24)
>>          epact++;
>>      return  Math.floor (((((diy + epact) * 6) + 11) % 177) / 22) % 8;
>>    }

Found this function to be from hacklib.c. The original hacklib function
is a little different, as it uses "TM Year" instead of "Year", no floor
rounding and the last mod division is by 7 rather than 8... I tested it,
and Janis' version of the function is more accurate, as far as the few
results I checked are concerned. I don't understand the formula thus IDK
why the hacklib function had to be updated. That being said, it ought to
be a reasonable function to use after I do some more testing!

hacklib.c: <https://nethackwiki.com/wiki/Source:NetHack_1.3d/unixunix.c>

castAway

castAway

unread,
Nov 15, 2022, 8:02:00 PM11/15/22
to
On 11/11/22 14:06, Janis Papanagnou wrote:
> And a final remark... - In this case you have to also consider calendar
> switches (Julian/Gregorian) in your date calculations. (Not sure your
> code supports that.)

We dont support such difference in calendar because I am not very much
familiar with the difference, we support UNIX time and ISO8601 dates
only, for as far as few centuries as static maths may go.

castAway

unread,
Nov 15, 2022, 8:14:24 PM11/15/22
to
On 11/11/22 13:36, Janis Papanagnou wrote:

> function phase_of_the_moon (now) // 0-7, with 0: new, 4: full

Bash integer arithmetics do floor rounding by deafults, don't trust,
check it, thus $(( 7/4 )) returns 1... Like Ksh, so maybe the floor rounding
function you mentioned is an overhead... Maybe in Java scripting all integers
are calculated as floating point?


castAway

unread,
Nov 15, 2022, 8:37:38 PM11/15/22
to
On 11/11/22 13:36, Janis Papanagnou wrote:

> function phase_of_the_moon (now) // 0-7, with 0: new, 4: full

OMG, did you just copy & paste this function ? Well, that seems intuitive,
given references

Keith Thompson

unread,
Nov 15, 2022, 9:59:07 PM11/15/22
to
For many purposes, the Proleptic Gregorian Calendar is probably
reasonable, and apparently the ISO 8601 standard explicitly requires it.
It extends the Gregorian leap-year rules into the past, before its
actual introduction. It avoids a discontinuity, but dates in the far
past will differ from what they would have been called at the time.

The transition from Julian to Gregorian happened at different times in
different places, from 1582 up to the early 20th century.

https://en.wikipedia.org/wiki/Proleptic_Gregorian_calendar
https://en.wikipedia.org/wiki/List_of_adoption_dates_of_the_Gregorian_calendar_by_country

--
Keith Thompson (The_Other_Keith) Keith.S.T...@gmail.com
Working, but not speaking, for XCOM Labs
void Void(void) { Void(); } /* The recursive call of the void */

Ben Bacarisse

unread,
Nov 16, 2022, 3:13:01 PM11/16/22
to
castAway <n...@where.com> writes:

> On 11/11/22 13:36, Janis Papanagnou wrote:
>
>> function phase_of_the_moon (now) // 0-7, with 0: new, 4: full
>
> Bash integer arithmetics do floor rounding by deafults,

No. Bash's integer division truncates towards zero. The manual says
the results as "as in C", which presumably means modern C. In C90, x/y
with either operand negative was implementation defined. Since C99 the
result is truncated towards zero.

--
Ben.

Janis Papanagnou

unread,
Nov 16, 2022, 8:38:25 PM11/16/22
to
On 16.11.2022 02:14, castAway wrote:
> On 11/11/22 13:36, Janis Papanagnou wrote:
>
>> function phase_of_the_moon (now) // 0-7, with 0: new, 4: full
>
> Bash integer arithmetics do floor rounding by deafults, don't trust,

Sorry if not having been clear here; where I wrote "for a web page"
I indicated - if not already deducible by the syntax - that this is
Javascript code (not bash, not C, etc.)

function phase_of_the_moon (now) // 0-7, with 0: new, 4: full
{
var diy = day_in_year (now);
var goldn = (now.getFullYear() % 19) + 1;
var epact = (11 * goldn + 18) % 30;
if ((epact == 25 && goldn > 11) || epact == 24)
epact++;
return Math.floor (((((diy + epact) * 6) + 11) % 177) / 22) % 8;
}

> check it, thus $(( 7/4 )) returns 1... Like Ksh,

Note that Ksh does arithmetic in a context sensitive way; 7/4 is
integer arithmetic (and behaves as you say, while 7./4 or 7/4. or
7./4. or 7.0/4.0 does FP arithmetic (with result 1.75).

I haven't implemented or needed that moon-phase code in shell.

> so maybe the floor rounding
> function you mentioned is an overhead... Maybe in Java scripting all
> integers are calculated as floating point?

Don't ask me! - Javascript as language has many design flaws.
The reason I use it occasionally is to support dynamic web
content, and since I have no own webserver running to do that
processing server-side I use this simple client-side option.
Every time I use it (after longer breaks) I have to re-think
about Javascript's quirks. I seem to recall that the origin
of the algorithm was programmed in C with integer arithmetic
and the Math.floor() had been identified as necessary when I
transcribed it.

Janis

Janis Papanagnou

unread,
Nov 16, 2022, 8:47:14 PM11/16/22
to
Not copy/past but a straightforward transcription. Note that this is
not production code or code I published for others, so I don't need
references for my own use. But if you are interested I can provide
the comment from the C source code for you - here it is... (HTH)

/*
* moon period = 29.53058 days ~= 30, year = 365.2422 days
* days moon phase advances on first day of year compared to preceding year
* = 365.2422 - 12*29.53058 ~= 11
* years in Metonic cycle (time until same phases fall on the same days of
* the month) = 18.6 ~= 19
* moon phase on first day of year (epact) ~= (11*(year%19) + 29) % 30
* (29 as initial condition)
* current phase in days = first day phase + days elapsed in year
* 6 moons ~= 177 days
* 177 ~= 8 reported phases * 22
* + 11/22 for rounding
*/

As also said already, I also seem to recall that I had also found some
description in Wikipedia; if I recall that correctly you'll certainly
find it without my assistance.

Janis

Janis Papanagnou

unread,
Nov 16, 2022, 8:54:21 PM11/16/22
to
On 12.11.2022 16:40, castAway wrote:
> On 11/10/22 21:46, Janis Papanagnou wrote:
>> Looks to me like the mentioned date programs or a simple GNU awk date
>> implementation supports that already.
>
> [...]
>
> Now, I know that KSH is very fast and I also read that Ksh code is very
> convoluted
> or very difficult to read and maintain, for example, Mr Siteshwar and Mr
> Rader
> complained a lot of the Ksh code when they were at it trying to develop
> it further.
>
> So, it comes to me that Mr Korn sacrificed readability and
> maintainability in
> exchange for speed, or am I wrong?

I can just speculate here; my impression is that they implemented so
many new and non-trivial concepts (in addition to the optimizations)
that at some point they maybe neglected to maintain code quality as
first and IMO most important goal.

That's why I'd suggest to use the version "ks93 u+m" that Martijn
Dekker maintains; it is announced here in this newsgroup and fixed a
lot of bugs in addition to some modifications.

HTH.

Janis

>
> Cheers,
> JSN

Janis Papanagnou

unread,
Nov 16, 2022, 9:07:15 PM11/16/22
to
On 13.11.2022 21:23, castAway wrote:
> On 11/13/22 10:15, castAway wrote:
>> On 11/11/22 13:36, Janis Papanagnou wrote:
>>> I was recently looking at some code and there's probably similar
>>> cryptic formulas existing. I used an adapted _quantized_ version
>>> (only 8 phases, and a plain integer calculation) for a web page:
>>>
>>> function phase_of_the_moon (now) // 0-7, with 0: new, 4: full
>>> {
>>> var diy = day_in_year (now);
>>> var goldn = (now.getFullYear() % 19) + 1;
>>> var epact = (11 * goldn + 18) % 30;
>>> if ((epact == 25 && goldn > 11) || epact == 24)
>>> epact++;
>>> return Math.floor (((((diy + epact) * 6) + 11) % 177) / 22) % 8;
>>> }
>
> Found this function to be from hacklib.c.

Oh, you found exactly the same source package that I also used. :-)
(Mine is from a more contemporary version of Nethack, though, but this
part has obviously not changed since Nethack version 1.3.)

> The original hacklib function
> is a little different, as it uses "TM Year" instead of "Year", no floor
> rounding and the last mod division is by 7 rather than 8... I tested it,

No. The C source function is doing the last step as _binary_ operation
... & 7
which is equivalent here to the _arithmetic_ counterpart
... % 8
that uses the modulo operator (as opposed to bit-wise 'and').

Note that the hacklib.c code is _very old_ (probably many decades; I'm
too lazy to check the exact date), so a speculation is that the bit-wise
'and' was considered a speed optimization (over modulo) at these times.

Janis

Janis Papanagnou

unread,
Nov 16, 2022, 9:36:15 PM11/16/22
to
On 12.11.2022 16:40, castAway wrote:
>
> However, the functionality seems to be very basic:
>
> % $AST/date -E '2002-01-01' '2012-01-01'
> 9Y11M
> % $AST/date -E '12:01:01' '19:02:02'
> 7h01m
> % $AST/date -E '2002-01-01 12:01:01' '2012-01-01 19:01:01'
> 9Y11M

What does it return if you provide ISO dates?

$AST/date -E '2002-01-01T12:01:01' '2012-01-01T19:01:01'


Janis

Janis Papanagnou

unread,
Nov 16, 2022, 10:11:07 PM11/16/22
to
Nevermind. I found an AST date in some forgotten directory and the
result is the same. Seems we'd need two calls for sub-day accuracy,
and some formatting to create correctly formatted ISO time periods.

> Janis
>

Janis Papanagnou

unread,
Nov 16, 2022, 10:19:26 PM11/16/22
to
And the man page (strelapsed.3) says:

The two largest time units are used, limiting the return value length
to at most 6 characters.

>
>> Janis
>>
>

castAway

unread,
Nov 17, 2022, 8:51:14 PM11/17/22
to
On 11/16/22 23:07, Janis Papanagnou wrote:
> No. The C source function is doing the last step as _binary_ operation
> ... & 7
> which is equivalent here to the _arithmetic_ counterpart
> ... % 8
> that uses the modulo operator (as opposed to bit-wise 'and').

Thanks, I had missed the operator was different! The explanation
could not be any clearer! I did some testing and could not find
differences in results using and not using the floor() function of Ksh.

castAway

unread,
Nov 18, 2022, 6:57:02 PM11/18/22
to
Thanks for the clarification! if the function was written in C code and
Bash is as in C, then they should behave similarly.

Lew Pitcher

unread,
Nov 18, 2022, 9:14:32 PM11/18/22
to
Not necessarily, because, even with the C language's inherent
limitations, you can still write C code to do almost anything.

For instance, the GNU Multiple Precision Arithmetic Library (which
provides an API to perform arbitrary precision arithmetic, operating on
signed integers, rational numbers, and floating-point numbers) is written
in C. And, even though the GMP library is "written in C code", it
certainly does NOT "behave similarly" to C language arithmetic.

--
Lew Pitcher
"In Skills, We Trust"

castAway

unread,
Nov 19, 2022, 7:42:35 AM11/19/22
to
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


Janis Papanagnou

unread,
Nov 19, 2022, 8:40:14 AM11/19/22
to
On 19.11.2022 13:42, castAway wrote:
> Ksh93 `superior'
> parameter scoping in functions was a litte hard to deal with [...]

Note that ksh's 'typeset' specifies variables' meta-attributes.
The local scope in functions is just one, so it's not comparable
with bash's simple 'local' keyword. Any ksh's 'typeset' is also
not that portable. As mentioned upthread there's also the more
portable f()(...) instead of f(){...;} if all you want is
local-scoped variables.

> [...] Specially, the new friday_13th() function
> may still need some more work.

Is there anything more about that function than just checking the
day-of-week (Friday) and date-in-month (13)?

> I reckon it would be cool to have a shell function to convert UNIX times to
> ISO-8601 (and RFC-5322) formats.

You mean to convert "seconds since 'Unix Epoch'" to e.g. ISO time?
A lot of things can be dome with ksh's built-in printf function and
its "%(...)T" specifier. You can resort to GNU date or GNU awk for
other time functions, e.g.

$ awk -v s=1668864108 'BEGIN{print strftime("%FT%T",s)}'
2022-11-19T14:21:48

but I'm not sure about your portability requirements and GNU tools
might not be available.

I'm also still unsure about the supported date ranges. I a post
quite some time ago I posted some observations with the different
ranges of time functions in 'date', 'ksh', and 'awk'; all we seems
to be able to rely on was (IIRC) the rather short Unix-Epoch range.

Janis

John-Paul Stewart

unread,
Nov 19, 2022, 11:28:03 AM11/19/22
to
Yes, in JavaScript all numbers are indeed floating point. Well, unless
you explicitly use the BigInt type:

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Numbers_and_dates#numbers

castAway

unread,
Nov 19, 2022, 11:37:30 AM11/19/22
to
On 11/19/22 10:40, Janis Papanagnou wrote:
> As mentioned upthread there's also the more
> portable f()(...) instead of f(){...;} if all you want is
> local-scoped variables.

I had missed that subtlety in the thread. The f() (...) syntax may
make the script run slower but maxes out compatibility, that is very
useful to learn!

>
> I'm also still unsure about the supported date ranges. I a post
> quite some time ago I posted some observations with the different
> ranges of time functions in 'date', 'ksh', and 'awk'; all we seems
> to be able to rely on was (IIRC) the rather short Unix-Epoch range.

Indeed, I remember having read the thread `Range of dates' (15 Jan 2021)
after a search on Usenet at the start of this year about ranges of
times. My datediff.sh script does not rely on `date' because I had
seen some narrow calendrical limits of `date' depending on the system
(`date' run under Termux has narrower time ranges, if I remember
correctly). The script follows the proleptic Gregorian calendar,
as already said, and there is just one adjustment I could think of
to get Julian dates that is skipping year 0000, but that would differ
from `date' command logic.

% date -u -d 0000-01-01 +%s
-62167219200

% datediff.sh -- -0001-01-01 -0000-01-01
DATES##
-001-01-01T00:00:00+00 -62198755200
0000-01-01T00:00:00+00 -62167219200
RANGES
1Y 00M 00W 00D 00h 00m 00s
1.0 year | 12.0 months | 52.1 weeks | 365.0 days | 8760.0 hours | 525600.0 mins | 31536000 secs

In the example above, `date' will be warped to process input dates
into UNIX times. However, GNU date will fail and the script will,
first, calculate the time elapsed between both dates, and then calculate
the time interval/elapsed from the closest year to UNIX epoch zero
time. That way, that is possible to generate UINX times with
arithmetics, as we count all days between these dates to get the
time in seconds.

> Is there anything more about that function than just checking the
> day-of-week (Friday) and date-in-month (13)?

You can also set positional arguments to get the combination of
day-of-week and date-in-month, for example:

% datediff.sh -F 1 mon
Mon, 01 May 2023 is 163 days ahead


Cheers,
JSN

castAway

unread,
Nov 19, 2022, 12:02:44 PM11/19/22
to
On 11/19/22 13:37, castAway wrote:

Sorry, in the previous example, `date' will fail but shell
time function is still used to get current time. For the
manual UNIX time generation facility to work, the running
shell must not support built-in time functions (must be
earlier than bash 4.2, for example). To force manual UNIX
time generation in the script, set flags -DD to disable
`date' and shell time built-ins wrapping.


% datediff.sh -DD -- -0001-01-01 -0000-01-01
DATES##
-001-01-01T00:00:00+00 -62198755200
0000-01-01T00:00:00+00 -62167219200
RANGES
1Y 00M 00W 00D 00h 00m 00s
1.0 year | 12.0 months | 52.1 weeks | 365.0 days | 8760.0 hours | 525600.0 mins | 31536000 secs


JSN

castAway

unread,
Nov 19, 2022, 12:06:39 PM11/19/22
to
No, in the previous examples, both will require manual
UNIX time generation. Shell time facility is only used to
print RFC timestamps, and get current time if any date1
or date2 is not set by user input. So in those previous
e-mail examples I just sent, it does not matter if using
flag -DD or not.

Janis Papanagnou

unread,
Nov 19, 2022, 2:03:57 PM11/19/22
to
On 19.11.2022 13:42, castAway wrote:
> I am afraid to have gone a little over the top. The lunar phase function
> was implemented, [...]

> #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

One thing I forgot that I wanted to point out...
We should be aware that the 8-value quantization will result in phases
of 3 or 4 consecutive days with the same moon phase. I noticed that in
the game of Nethack (where that code stems from) the "Nethack new moon"
phase _starts_ at the day when _real_ new moon actually is. That might
not be what one expects, though. At least my expectation was that it
would be better to either center the real moon phase date around these
3-4 days phase, or give up the quantization and calculate it on a 29.5
days per month basis.

Janis

castAway

unread,
Nov 19, 2022, 7:03:29 PM11/19/22
to
On 11/19/22 10:40, Janis Papanagnou wrote:
> I'm also still unsure about the supported date ranges. I a post
> quite some time ago I posted some observations with the different
> ranges of time functions in 'date', 'ksh', and 'awk'; all we seems
> to be able to rely on was (IIRC) the rather short Unix-Epoch range.
I am mulling over about shell arithmetics limits.

As the script counts all days between dates and then sums up it all as
seconds before doing divisions to get single unit time intervals, such
as 60.5 years, the FP result incurs in overflow at about 68. years worth
of time.

Some testing reveals that my script can only count up to 2147483647 seconds
(about 68 years), for the FP results of single units of time. That number
is one decrement below 2^32 / 2 = 4294967296 / 2 = 2147483648, because
there is no bits left for a double point after that...

Spurious negative results should exit with 1, but I just check the compound
time range, for negative results (which should not happen!).

That is one of the reasons I decided to use Bc to calculate those FPs,
in retrospective.

The compound time interval, and the UNIX time generation facility of
the script, though, should work for much larger amounts of time.

Those functions work with shell integer arithmetics wich have got a limit
of 19 digits. It should work with time intervals up to close to
9,223,372,036,854,775,807 seconds, which is about 292 billions years!

And, if we use Bc, we can get FP single unit results within that limit
as well... I will revert to using Bc for single unit time ranges by
defaults and add a note on a LIMITS section of the help page!

A Bc script could calculate within much broader FP and integer limits.

### MaxScale - Get the maximum scale from your GNU BC
### BC is currently only 32bit in all cases.
### The package manager recognizes that it might have to install
### 32bit libraries for a 64 bit environment, but continue to be
### 32 bits libraries.
### That is why your maximum scale is always 2147483647 (though
### is safer to use 2147483647, because by reason of the minus or
### the decimal symbol in case of negative or decimal operations
### by reason of the minus and decimal symbol)
### 1111111111111111111111111111110 bits.
### If you try to add just 1 bit more, you will get a fatal error
### 'Out of memory for malloc'.
### Which explain that more memory can't be allocated, even if there
### is not an overflow because you are running a >32 bits computer.
### If you want to use bc in bash
### (such as $(echo "scale=2147483646;1/6" | bc) | tee -a division)
### you will get 'bash: xrealloc: cannot allocate 18446744071562067968
### bytes;'
### (2^31 digits represent A LOT of bytes for a normal 32/64 bits
### computer memory!) and 'Out of memory malloc' in case you want
### to assign the value to a variable or simply copy and paste
### outside bc. However, you can still operate with such a large
### number inside bc without problems while you don't exceed 2^31
### digits. Isn't that awesome?"
#scale=2147483646

Cheers,
JSN

castAway

unread,
Nov 19, 2022, 9:12:54 PM11/19/22
to
On 11/19/22 21:03, castAway wrote:
> On 11/19/22 10:40, Janis Papanagnou wrote:
>> [...]

Well, I was testing the script under Termux in my Android mobile phone.
shell arithmetics seem to perform much better (i.e. larger limits for FP)
under Linux 64bit i7 cpu.

JSN

castAway

unread,
Nov 20, 2022, 4:58:19 AM11/20/22
to
On 11/19/22 16:03, Janis Papanagnou wrote:
> One thing I forgot that I wanted to point out...
> We should be aware that the 8-value quantization will result in phases
> of 3 or 4 consecutive days with the same moon phase. I noticed that in
> the game of Nethack (where that code stems from) the "Nethack new moon"
> phase _starts_ at the day when _real_ new moon actually is. That might
> not be what one expects, though. At least my expectation was that it
> would be better to either center the real moon phase date around these
> 3-4 days phase, or give up the quantization and calculate it on a 29.5
> days per month basis.
>

I think the moon phase is a little subjective as brightness of the moon
varies from the observed GPS location in the globe.


Janis Papanagnou

unread,
Nov 20, 2022, 9:30:52 AM11/20/22
to
On 20.11.2022 10:58, castAway wrote:
>
> I think the moon phase is a little subjective as brightness of the moon
> varies from the observed GPS location in the globe.

The moon phase is primarily depending the position of moon and sun as
seen from earth; if (for example) moon and sun are in an orthogonal
angle you have a half moon phase. Because of the magnitude of actual
distances and diameters ([avg.] sun ~150'000'000 km, moon ~184'000 km,
earth diameter ~12'700 km) the concrete observation position on earth
is not significant.

S M
E

The very rough 8-value discretization of moon phases that I mentioned
that lead to phases of 3-4 days(!) is quite significant. And the other
consequence - what is a sensible definition - is even more important;

|---------phase---------|
day 1 day 2 day 3 day 4
+-----+-----+-----+-----+
^ ^
a b

whether, say, new moon is defined as starting the quantized phase (a)
or being defined as the mid of the phase (b) means a difference in
accuracy of 1.5 or 2.0 days.

Janis

castAway

unread,
Nov 23, 2022, 8:07:33 PM11/23/22
to
On 11/20/22 11:30, Janis Papanagnou wrote:
> The moon phase is primarily depending the position of moon and sun as
> seen from earth; if (for example) moon and sun are in an orthogonal
> angle you have a half moon phase. Because of the magnitude of actual
> distances and diameters ([avg.] sun ~150'000'000 km, moon ~184'000 km,
> earth diameter ~12'700 km) the concrete observation position on earth
> is not significant.
>
> S M
> E

My thoughts, exactly!

You know we cannot do anything about improving this formula, right?

And yes, brightness does depend on the observer position on Earth,
although Lunar Phases shouldn't because there are precise angles
of Sun & Moon longitudes from Earth which should define moon phases...

Janis Papanagnou

unread,
Nov 24, 2022, 3:28:40 AM11/24/22
to
On 24.11.2022 02:07, castAway wrote:
>
> You know we cannot do anything about improving this formula, right?

The quantization[*] could be removed from the formula so that we have
a single matching day [for full moon and new moon], or/and vice versa,
that a single exact date/time result is provided for a specific phase
like these, and provide percentages relative to new/full moon instead
of 8 phase values of 3-4 days each. (Just to name some alternatives.)

That said; I want to recall that it's not necessary a sensible feature
for such a small library. (YMMV, of course.) It's a question of what
features are sensible here - a choice you have to make! A "complete"
date/time library leads probably to a doomed project, considering all
the difficulties, asymmetries, quirks, incoherences, model types, etc.
(Leap-years and leap-seconds, date range coverage, different calender
types, different historic introduction dates, different special dates
(like easter), time zones, set of sensible date functions, flexibility
with date representations (input and output), mapping to astronomical
time/date definitions, etc. - date and time is nothing simple, even if
it appears so at first glance. - OTOH, date and time is so fundamental
that I wonder whether there's already some "complete" library existing
and published?)

Janis

[*] Which is okay for the game of Nethack, where the formula stems from.


Janis Papanagnou

unread,
Nov 24, 2022, 3:31:42 AM11/24/22
to
On 20.11.2022 15:30, Janis Papanagnou wrote:
> distances and diameters ([avg.] sun ~150'000'000 km, moon ~184'000 km,
> earth diameter ~12'700 km) [...]

Sorry, it just occurred to me, a typo; must be: moon ~384'000 km

Janis

castAway

unread,
Nov 30, 2022, 1:01:27 AM11/30/22
to
An older version of this moon phase function, or a Java Script version
of it, it used to subtract 1900 from whatever year was input, so for
e.g. year 1970 would be the same as year number 70 (1970-1900).

For this reason, I thought it may be feasible to adjust this formula
by adding or subtracting some years for the internal calculation.
In the NetHack comment for this formula, it notes the following:


#days moon phase advances on first day of year compared
#to preceding year
# = 365.2422 - 12*29.53058 ~= 11
#years in Metonic cycle (time until same phases fall on
#the same days of the month)
# = 18.6 ~= 19


It means the reading frame of the moon phase can be adjusted if
one can find a good number to correct the input year for internal
calculation.

In order to verify what adjustment could be made, all data for lunar
phases from 1700 to 2102 was scraped from
<https://aa.usno.navy.mil/data/MoonPhases>.

Input year was then adjusted with a value from -200 to +200 years,
meaning that when input year is 1700, moon phases were calculated
as if it were {1500..1900}.

Generated tables were compared with that scraped from US Navy website.
That was possible to determine, within the tested interval of correction
from -200 to +200 years, that adding any `8', `-68', `84', `64' or `-144'
to user input produces more accurate results.

For example, it was possible to check that the original function as
was copied from modern NetHack matches exactly the US Navy dataset
3495 out of 19944 primary phases for the period between 1700-2102.

Adjustment Matches Non-matches
+0 3495 16449
+8 8037 11907
-144 8039 11905
-68 8047 11897
+84 8056 11888


The original formula can have up to 5 days or error, I found.
The following example shows the US Navy scrape information for
moon phases from May 1924. Original function returns results
up to 5 days out of sync. Adding 8 years to internal calculation
delivers much better results.


% grep 1924 ~/navy/all.txt | grep May
New Moon 1924 May 3 23:00
First Quarter 1924 May 12 02:14
Full Moon 1924 May 18 21:52
Last Quarter 1924 May 25 14:16

% datediff.sh -m 1924-05 | grep -v -e Wan -e Wax
1924-05-01 New Moon
1924-05-07 First Quarter
1924-05-15 Full Moon
1924-05-22 Last Quarter
1924-05-29 New Moon

~ % ADD_YEARS=8 datediff.sh -m 1924-05 | grep -v -e Wan -e Wax
1924-05-03 New Moon
1924-05-10 First Quarter
1924-05-18 Full Moon
1924-05-25 Last Quarter


As far as I can tell, it may be worth correcting the input year
for internal calculation, however deciding if the correction factor
should be `8' or `-144' is to me a little arbitrary. Even though `-144'
corrects a little better according to my tests, it may well be
that a value out of the tested range ( -200 >= tested range <= +200 )
may be even slightly better correction factor. This is just
a preliminary test.

Cheers,
JSN

applemcg

unread,
Dec 1, 2022, 10:12:56 AM12/1/22
to

a little late to the party, saw some discussion of function syntax, definition.

from this:

#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]}
}

i'd write:


get_day_in_week ()
{
: get day in the week
: example: get_day_in_week unix_time 2300000
: uses: DAY_OF_WEEK

echo ${DAY_OF_WEEK[( ( ($1+($1<0?1:0))/(24*60*60))%7 +($1<0?6:7))%7]}
}

this is the format returned by "declare -f", which i use as a shell-beautifier. it uses the null-command colon (:) as a comment, the first of which i call the abstract, a "shdoc" runs to the first non-Tag such lines, the "abstract" being the first such, where a Tag is ": {tag}: ". "date" is a common tag, in this case "example" is backed up by an eval_example function.

this has the advantage of dragging around the useful information with in the function itself. i've a ~ 2400 member function collection where i'm always looking for the right place to keep a function, including a "retiredlib". and an "app" is little more than a few high-level functions which gather up all the necessary functions, awk scripts, and text files.

castAway

unread,
Dec 1, 2022, 10:53:30 AM12/1/22
to
On 12/1/22 12:12, applemcg wrote:
>
> this is the format returned by "declare -f", which i use as a shell-beautifier. it uses the null-command colon (:) as a comment, the first of which i call the abstract, a "shdoc" runs to the first non-Tag such lines, the "abstract" being the first such, where a Tag is ": {tag}: ". "date" is a common tag, in this case "example" is backed up by an eval_example function.
>
> this has the advantage of dragging around the useful information with in the function itself. i've a ~ 2400 member function collection where i'm always looking for the right place to keep a function, including a "retiredlib". and an "app" is little more than a few high-level functions which gather up all the necessary functions, awk scripts, and text files.
>
>
I avoid using the : command to add a comment because it is run by the shell and makes the function run slower.

castAway

unread,
Dec 1, 2022, 12:41:03 PM12/1/22
to
On 12/1/22 12:12, applemcg wrote:
>
> this is the format returned by "declare -f", which i use as a shell-beautifier. it uses the null-command colon (:) as a comment, the first of which i call the abstract, a "shdoc" runs to the first non-Tag such lines, the "abstract" being the first such, where a Tag is ": {tag}: ". "date" is a common tag, in this case "example" is backed up by an eval_example function.
>
> this has the advantage of dragging around the useful information with in the function itself. i've a ~ 2400 member function collection where i'm always looking for the right place to keep a function, including a "retiredlib". and an "app" is little more than a few high-level functions which gather up all the necessary functions, awk scripts, and text files.
>
>

Janis Papanagnou

unread,
Dec 1, 2022, 4:50:49 PM12/1/22
to
On 01.12.2022 18:40, castAway wrote:
> On 12/1/22 12:12, applemcg wrote:
>>
>> this is the format returned by "declare -f", which i use as a
>> shell-beautifier. it uses the null-command colon (:) as a comment,
>> [...]
>>
> I avoid using the : command to add a comment because it is run by the
> shell and makes the function run slower.

I wouldn't consider the efficiency problem with that built-in as a
substantial speed degradation; it's trivial and a built-in command.
I'd primarily avoid it because it's a command and side-effects can
happen on the arguments or triggered by the arguments; for example

: We're using $(some_program) for efficiency here
: and ${var:=someval} for proper initialization.

where some_program would be run (probably costly) and with possible
undesired side-effects, and the second line manipulates var.

Another (minor) consideration is that your editor might not display
the text (that is intended as comment) as a comment.

Janis

Janis Papanagnou

unread,
Dec 1, 2022, 5:07:53 PM12/1/22
to
On 01.12.2022 16:12, applemcg wrote:
>
> a little late to the party, saw some discussion of function syntax, definition.
>
> from this:
>
> #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]}
> }
>
> i'd write:
>
>
> get_day_in_week ()
> {
> : get day in the week

Identical to function name, unnecessary bloat, a potential source of
inconsistency.

> : example: get_day_in_week unix_time 2300000

Seems to document a wrong syntax (seems to be inconsistent to code).

> : uses: DAY_OF_WEEK

Already quite obvious.

>
> echo ${DAY_OF_WEEK[( ( ($1+($1<0?1:0))/(24*60*60))%7 +($1<0?6:7))%7]}
> }
>

I'd probably write (using ksh syntax here, but similar for bash)

function get_day_in_week
{
typeset -i unix_time=${1:?}

printf "%s\n" ${DAY_OF_WEEK[ $(( unix_time + ... )) ]
}

and omit the unnecessary comments, add a variable for legibility and
safety (i.e. the test on $1), use arithmetic syntax for clarity, and
standard (and generally safer) printf.

> [...]

Mileages vary.

Janis

>
>
> On Wednesday, November 30, 2022 at 1:01:27 AM UTC-5, castAway wrote:
>> [...]


Janis Papanagnou

unread,
Dec 1, 2022, 5:11:50 PM12/1/22
to
On 01.12.2022 23:07, Janis Papanagnou wrote:
>
> printf "%s\n" ${DAY_OF_WEEK[ $(( unix_time + ... )) ]

printf "%s\n" "${DAY_OF_WEEK[ $(( unix_time + ... )) ]}"

(fix of syntax)

Janis

castAway

unread,
Dec 1, 2022, 6:24:30 PM12/1/22
to
On 11/20/22 11:30, Janis Papanagnou wrote:
> [...]
>>

As noted in the last e-mail about finding a correction/improvement
factor for the moon phase, there is an old variant of the moon
function which subtracts 1900 from the input year. For some reason,
the formula was updated and this adjustment removed.

This week, I tested the moon phase function was tested with correction
factor from -3800 to +3800 applied to the input year. Meaning that,
if input is year 2000, then calculation was tested with 7600 different
correction factors, such as
year (2000-1), (2001+1) ... (2000-3800) and (2000+3800)
to see whether these adjustments in the internal calculation would
improve results of moon phase according to data scraped from USNO Navy
for each year from 1700 to 2102. It basically checks results from the
script with correction factor applied to see if each moon phase starts
at the same DATE as that from USNO Navy tables.

Below is an abridged table with results from the tests with
correction factors used and ratio of exact matches to the Navy moon
phase starting dates.


Input Year | Exact | Non- | Ratio
Adjustment | Matches | Matches | (Percent)
------------+----------+-----------+----------
-1900 | 1800 | 18132 | 9.03%
------------+----------+-----------+----------
+0 | 3495 | 16449 | 17.52% [original]
------------+----------+-----------+----------
+8 | 8037 | 11907 | 40.30%
------------+----------+-----------+----------
-68 | 8047 | 11897 | 40.35%
------------+----------+-----------+----------
-1892 | 9508 | 10426 | 47.70%


Interestingly, subtracting 1892 from input year is the best adjustment
for this formula amongst the tested range. Improvement is observed
when testing correction factors grow up to |1892| and factors greater
than |1892| (positive and negative values) decrease improvement, so
it does seem like a correction factor of -1892 to the input year
ought to be optimal for this formula and use case, even though
the formula can `only' match the start of moon phases to USNO navy
tables at about 48% of times (better than the original formula which
delivers about 18% matches), but also, *it improves accuracy* of overall
results, i.e. decreases the distance from the start of calculated
moon phases relative to USNO Navy tables.

I hope this test explanation is comprehensible/understandable.

This moon phase function testing generated 3.34 GB of data which was
compared to Navy data, so I am reasonably confident about the results.

Cheers!
JSN





applemcg

unread,
Dec 4, 2022, 5:36:58 PM12/4/22
to

applemcg

unread,
Dec 4, 2022, 6:50:43 PM12/4/22
to
Janis,

thanks for your helpful corrections.

thanks also for pointing out the performance hit isn't to severe

but let me object to the use of "bloat"

i'm working on an idea, where the shell source text, the stuff you
put in source control, has the "unnecessary bloat", and you've
given be a terrific idea.

i've library, "shdoclib" which mangles shell function text. the
syntax you're looking at is a feature i call "tags"

a sample tag, "date" will appear like this in the code:

somefunction ()
{
: "the first null command is the function's abstract"
: here quoted to protect the " ' " character
: after the shdoc, which are the first untagged-lines of the function, come the tags,
: such as the next line
: date: 2016-06-06, the first time we stored this in a library
: ... etc ...
: date: 2022-12-04, the most recent modification
... # followed by the body of the function
...
}

w.r.t. the "example" example, that was indeed a syntactic mistake,

it might have more correctly said:

: example: get_day_in_week 1670194179

the purpose of the example tag is to show an instance of usage, if
only as a reminder. it may _not_ be directly using the function.
Here's an (dare I say) worked example, not every function is here
presented, but...

$ def {eval,show}_example show_dateformat
eval_example ()
{
: evaluate, i.e. execute the example TAG from a function;
: example: eval_example show_dateformat;
: related: show_dateformat;
: date: 2022-09-24;
: date: 2022-10-03;
: date: 2022-11-18;
set -- $(show_example $1);
debug $# $*;
[[ -n "$1" ]] && eval $@
}
show_example ()
{
: extract the EXAMPLE tag value, i.e. the string to execute;
: date: 2022-09-24;
: date: 2022-11-18;
if_missingargs 1 $* && return;
report_notfunction $1 && return 1;
declare -f $1 | grep ': example: ' | sed "

s/[^']*: example: *//
s/; *$//
"
}
show_dateformat ()
{
: example: foreach show_dateformat {a..z} {A..Z} | awk '$1 != $2';
: related: eval_example;
: date: 2020-05-03;
: date: 2022-09-23;
: date: 2022-11-18;
printf "%s\t%s\n" $1 "$(date +%$1)"
}
shlib.$ eval_example eval_example
a Sun
b Dec
c Sun Dec 4 17:54:19 2022
d 04
e 4
g 22
h Dec
j 338
k 17
l 5
m 12
n
p PM
r 05:54:20 PM
s 1670194460
t
u 7
v 4-Dec-2022
w 0
x 12/04/2022
y 22
z -0500
A Sunday
B December
C 20
D 12/04/22
F 2022-12-04
G 2022
H 17
I 05
M 54
R 17:54
S 20
T 17:54:20
U 49
V 48
W 48
X 17:54:20
Y 2022
Z EST
shlib.$

Another tag is "related" means- a function not in the body of the code

other functions:

* if_missingargs, returns an error if the count of $* is less than the
preceeding N

* debug, with related "pause" function; these turned "on" and "off"

* report_notfunction, returns TRUE if it's argument is NOT a
function, reporting on stderr

Now, to solve your "unnecessary bloat" problem, I'm already keeping a
duplicate copy of every function library around, so, with a little
care, the one which i execute will be run thru a yet-to-be-written
"just_nocolon" function, a complement of this "shd_justcolon" function:


$ def shd_justcolon
shd_justcolon ()
{
: returns leading colon-comments from a SINGLE function;
: date: 2020-08-28;
report_notpipe && return 1;
awk '
NR > 2 {
if ( $1 !~ /^:/ ) exit
else print
}
'
}
shlib.$

The copy on my PATH will be the NO-colon copy, the copy i work, test
and record the tag data base with will be HAS-colon copy. And, if you
don't mind, I shall credit you with the insight.

At my advanced age, my time is infinitely more precious than computer
time, so i thank you for skewering any "performance" argument. One
way I improve my work is to make abstracts of function libraries and
use them when it's time to recall what I may need now.

Here's a sample of querying my Database of Functions with Grep (dfg),
to produce a truncated TAG report, first TAG only:


$ dfg .| column context | row 'context ~ /: [a-zA-A0-9]*: /' | rd awk '!p[$2]++'

context
-------
: date: 2022-08-01;
: todo: construct the copyright dates from the date tag.;
: usage: "app_isa arg ... || return";
: lesson: how easy this all is;
: example: comm <( command_all|tpl|sort) <( functions ./commandlib | sort);
: related: eval_example;
...
shlib.$

I use a quite old copy of the Unix Relational Data Base /RBD (search for it) where

* column extracts the named columns, defaulting to all,
* row is the "select" operation, in this case the "context" column is a TAG, and
* rd is my hack to leave the RDB column headers, while executing the command arguemnts

Here's the tail of a "Word count" on the development database


...

10 53 381 utillib/wheres_home
6 19 127 utillib/whf
6 15 98 utillib/wpl
4 5 24 xrflib/bar
4 5 24 xrflib/foo
12 44 299 xrflib/gather_aux
16 65 473 xrflib/obsolescent
4 5 24 xrflib/zot
21565 73931 523977 total
dir.$
dir.$ wc */* | wc -l
1982
dir.$


N.B. you can imagine how valuable "related" and "use" become in a function database.

again, thanks for the insight.

~ Marty (aka, applemcg, a McGowan from Appleton (MN, not WI))

p.s. a quick scan of the resulting WC suggests a population history of the line count (WC -L)
for each function. never had thought of that ''till now. that's two for you.






On Thursday, December 1, 2022 at 5:07:53 PM UTC-5, Janis Papanagnou wrote:

Janis Papanagnou

unread,
Dec 4, 2022, 10:32:13 PM12/4/22
to
On 05.12.2022 00:50, applemcg wrote:
>
> thanks also for pointing out the performance hit isn't to severe

Note that the side effects on ':' lines may be severe, as demonstrated.

Why don't you use comments for that purpose? I mean, other languages
seem to have done so, and if there's some need for "special comments"
you can build on the existing ones, like using '#>' (instead of '#')
so that you anticipate effects of ':' side-effects and special care.

>
> but let me object to the use of "bloat"
>
> i'm working on an idea, where the shell source text, the stuff you
> put in source control, has the "unnecessary bloat", and you've
> given be a terrific idea.
>
> i've library, "shdoclib" which mangles shell function text. the
> syntax you're looking at is a feature i call "tags"

This is nothing new you are explaining here. These things are supported
by (quasi-standard) tools like eg. javadoc that I used in my Java times,
and before that, three decades ago, I defined standard document headers
(for e.g. C++ and Kornshell) with tags that were used company-wide then.

If all you want is (user-defined, programmer-defined) tags just do it;
I understand and acknowledge that.
Although nowadays I'd most likely use some standard tool - unless my
requirements would be so trivial that some own solution would appear
more appealing (e.g. to avoid overhead/dependencies on external tools).

I was referring to the unnecessary comments that are widely depreciated
(AFAICT).

Typical examples are; repetitions of existing functional information
function get_day_in_week # get day in week
having information at the "wrong" place and requiring searches/lookups
function f # function f is returning the day in week
typeset a # used to store area of rectangle
(( a = $1 * $2 )) # width * height
documenting unnecessary information like language explanations
pi=3.14 # we assign value of 'pi' for more terse usage
and some more.

You've demonstrated my point already that inconsistencies may slip in
if you duplicate code (or things that belong to code) in comments.

> [...]

>
> Now, to solve your "unnecessary bloat" problem, I'm already keeping a
> duplicate copy of every function library around, so, with a little
> care, the one which i execute will be run thru a yet-to-be-written
> "just_nocolon" function, a complement of this "shd_justcolon" function:

You are keeping a copy? And need some care doing something? Finally
create a non-colon function? - Seems I miss the advantage of doing
all that. Anyway.

Note: there's nothing wrong if you have the need for that, for tags.
We are probably just speaking about different things. While you seem
to have advertised your tag-based commenting for your purposes I was
just providing a mundane alternative for writing shell code with IMO
less maintenance issues.

For standard documentation and it's extraction I use the existing
operational language features to express things. For everything
beyond that I add use (preferred standard) tools that support that.
Additional necessary comments to understand things not expressed
by the language, nor by the use of proper names and identifiers.

My experience with tags is that you need QA measures to keep them
true, consistent, sufficiently complete, and generally useful.

Janis

castAway

unread,
Dec 4, 2022, 11:40:35 PM12/4/22
to
On 12/4/22 20:50, applemcg wrote:

the exec time may not be affected by running the : command however
painful debug prints this command expansion, with set -xv for eg

castAway

unread,
Dec 4, 2022, 11:44:07 PM12/4/22
to
On 12/4/22 20:50, applemcg wrote:
> somefunction ()
> {
> : "the first null command is the function's abstract"
> : here quoted to protect the " ' " character
> : after the shdoc, which are the first untagged-lines of the function, come the tags,
> : such as the next line
> : date: 2016-06-06, the first time we stored this in a library
> : ... etc ...
> : date: 2022-12-04, the most recent modification
> ... # followed by the body of the function
> ...
> }

all the : forward code is unnecessary and will be printed on debugging

also, i don't care about comments of the source code if the
reference can be trusted blindly

Janis Papanagnou

unread,
Dec 5, 2022, 3:06:10 AM12/5/22
to
I may have a different opinion on blind trust, but now that I see this
comment piece above isolated I get the impression that at least the
'date:' stuff looks very much like configuration management / version
control stuff that is usually managed outside of source code files
(with the check-in process by the respective tools).

Janis

Jalen Q

unread,
Mar 5, 2023, 7:54:50 PM3/5/23
to
On Friday, November 11, 2022 at 11:04:19 AM UTC-6, Janis Papanagnou wrote:
> On 11.11.2022 17:36, Janis Papanagnou wrote:
> > On 11.11.2022 04:34, castAway wrote:
> >> On 11/10/22 21:46, Janis Papanagnou wrote:
> >>> What does the script that AT&T date or GNU date doesn't support in this
> >>> (time-intervals) respect?
> >>
> >> I am not sure that `awk' or `date' can be used to calculate time intervals
> >> other than in days, hours, minutes and seconds. So how can you calculate
> >> these time differences with awk? You could get to an _approximate_ result
> >> very easily but it gets more difficult as we need to compensate for a lot
> >> of things, for example not all months have 4 weeks.
> >
> > [...]
>
> On re-reading it occurred to me that you were probably not focusing
> on the sub-second issue but on support for dates before, say, 1900 or
> after 2100, or so. - Yes, Unix tools have different (often restricted)
> support for dates outside the Unix Epoch. - So, yes, depending on the
> used tool you'd have to transform the external form to an internal
> representation that fits this accuracy.
>
> Janis
0 new messages