• Datediff script (1/2)

    From castAway@21:1/5 to All on Thu Nov 10 19:33:21 2022
    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) {

    [continued in next message]

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Janis Papanagnou@21:1/5 to castAway on Fri Nov 11 01:46:56 2022
    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

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From castAway@21:1/5 to Janis Papanagnou on Fri Nov 11 00:34:37 2022
    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.

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Spiros Bousbouras@21:1/5 to castAway on Fri Nov 11 10:38:12 2022
    On Fri, 11 Nov 2022 00:34:37 -0300
    castAway <no@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).

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From castAway@21:1/5 to Spiros Bousbouras on Fri Nov 11 12:38:12 2022
    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.

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Janis Papanagnou@21:1/5 to castAway on Fri Nov 11 17:36:46 2022
    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.


    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.

    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

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Janis Papanagnou@21:1/5 to Janis Papanagnou on Fri Nov 11 18:06:40 2022
    On 11.11.2022 18:04, 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.

    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

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Janis Papanagnou@21:1/5 to Janis Papanagnou on Fri Nov 11 18:04:12 2022
    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

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From castAway@21:1/5 to Janis Papanagnou on Fri Nov 11 16:15:04 2022
    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.

    Based
    on one's own expertise folks have to judge themselves whether it fits
    them.

    Janis


    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Janis Papanagnou@21:1/5 to castAway on Sat Nov 12 00:27:31 2022
    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

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Chris Elvidge@21:1/5 to Janis Papanagnou on Sat Nov 12 12:07:15 2022
    On 11/11/2022 23:27, Janis Papanagnou wrote:
    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


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



    --
    Chris Elvidge
    England

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Spiros Bousbouras@21:1/5 to Chris Elvidge on Sat Nov 12 13:33:48 2022
    On Sat, 12 Nov 2022 12:07:15 +0000
    Chris Elvidge <chris@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.

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From castAway@21:1/5 to Janis Papanagnou on Sat Nov 12 12:40:16 2022
    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

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From castAway@21:1/5 to Janis Papanagnou on Sun Nov 13 10:15:47 2022
    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

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From castAway@21:1/5 to Chris Elvidge on Sun Nov 13 10:19:39 2022
    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.

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From castAway@21:1/5 to castAway on Sun Nov 13 17:23:34 2022
    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

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From castAway@21:1/5 to Janis Papanagnou on Tue Nov 15 22:01:53 2022
    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.

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From castAway@21:1/5 to Janis Papanagnou on Tue Nov 15 22:14:19 2022
    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?

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From castAway@21:1/5 to Janis Papanagnou on Tue Nov 15 22:37:33 2022
    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

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Keith Thompson@21:1/5 to castAway on Tue Nov 15 18:58:56 2022
    castAway <no@where.com> writes:
    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.

    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.Thompson+u@gmail.com
    Working, but not speaking, for XCOM Labs
    void Void(void) { Void(); } /* The recursive call of the void */

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Ben Bacarisse@21:1/5 to castAway on Wed Nov 16 20:12:56 2022
    castAway <no@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.

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Janis Papanagnou@21:1/5 to castAway on Thu Nov 17 03:07:10 2022
    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

    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


    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Janis Papanagnou@21:1/5 to castAway on Thu Nov 17 02:47:09 2022
    On 16.11.2022 02:37, castAway wrote:
    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

    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

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Janis Papanagnou@21:1/5 to castAway on Thu Nov 17 02:38:19 2022
    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

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Janis Papanagnou@21:1/5 to castAway on Thu Nov 17 02:54:16 2022
    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

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Janis Papanagnou@21:1/5 to castAway on Thu Nov 17 03:36:09 2022
    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

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Janis Papanagnou@21:1/5 to Janis Papanagnou on Thu Nov 17 04:11:02 2022
    On 17.11.2022 03:36, Janis Papanagnou wrote:
    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'

    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


    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Janis Papanagnou@21:1/5 to Janis Papanagnou on Thu Nov 17 04:19:20 2022
    On 17.11.2022 04:11, Janis Papanagnou wrote:
    On 17.11.2022 03:36, Janis Papanagnou wrote:
    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'

    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.

    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



    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From castAway@21:1/5 to Janis Papanagnou on Thu Nov 17 22:51:05 2022
    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.

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From castAway@21:1/5 to Ben Bacarisse on Fri Nov 18 20:56:54 2022
    On 11/16/22 17:12, Ben Bacarisse wrote:
    castAway <no@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.


    Thanks for the clarification! if the function was written in C code and
    Bash is as in C, then they should behave similarly.

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Lew Pitcher@21:1/5 to castAway on Sat Nov 19 02:14:26 2022
    On Fri, 18 Nov 2022 20:56:54 -0300, castAway wrote:

    On 11/16/22 17:12, Ben Bacarisse wrote:
    castAway <no@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.


    Thanks for the clarification! if the function was written in C code and
    Bash is as in C, then they should behave similarly.

    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"

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From castAway@21:1/5 to All on Sat Nov 19 09:42:28 2022
    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=


    [continued in next message]

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Janis Papanagnou@21:1/5 to castAway on Sat Nov 19 14:40:08 2022
    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

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From castAway@21:1/5 to Janis Papanagnou on Sat Nov 19 13:37:23 2022
    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

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From John-Paul Stewart@21:1/5 to castAway on Sat Nov 19 11:27:25 2022
    On 2022-11-15 20: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,
    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?

    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

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From castAway@21:1/5 to All on Sat Nov 19 14:02:39 2022
    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

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From castAway@21:1/5 to All on Sat Nov 19 14:06:34 2022
    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.

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Janis Papanagnou@21:1/5 to castAway on Sat Nov 19 20:03:52 2022
    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

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From castAway@21:1/5 to Janis Papanagnou on Sat Nov 19 21:03:22 2022
    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

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From castAway@21:1/5 to castAway on Sat Nov 19 23:12:48 2022
    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

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From castAway@21:1/5 to Janis Papanagnou on Sun Nov 20 06:58:13 2022
    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.

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Janis Papanagnou@21:1/5 to castAway on Sun Nov 20 15:30:46 2022
    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

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From castAway@21:1/5 to Janis Papanagnou on Wed Nov 23 22:07:27 2022
    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...

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Janis Papanagnou@21:1/5 to castAway on Thu Nov 24 09:28:34 2022
    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.

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Janis Papanagnou@21:1/5 to Janis Papanagnou on Thu Nov 24 09:31:36 2022
    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

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From castAway@21:1/5 to All on Wed Nov 30 03:01:14 2022
    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

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From applemcg@21:1/5 to i'd on Thu Dec 1 07:12:52 2022
    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.


    On Wednesday, November 30, 2022 at 1:01:27 AM UTC-5, castAway wrote:
    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

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From castAway@21:1/5 to applemcg on Thu Dec 1 12:53:23 2022
    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.

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From castAway@21:1/5 to applemcg on Thu Dec 1 14:40:56 2022
    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.

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Janis Papanagnou@21:1/5 to castAway on Thu Dec 1 22:50:43 2022
    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

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Janis Papanagnou@21:1/5 to applemcg on Thu Dec 1 23:07:48 2022
    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:
    [...]

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Janis Papanagnou@21:1/5 to Janis Papanagnou on Thu Dec 1 23:11:44 2022
    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

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From castAway@21:1/5 to Janis Papanagnou on Thu Dec 1 20:24:23 2022
    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

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From applemcg@21:1/5 to Janis Papanagnou on Sun Dec 4 14:36:55 2022
    On Thursday, December 1, 2022 at 5:07:53 PM UTC-5, Janis Papanagnou wrote:
    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:
    [...]

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From applemcg@21:1/5 to Janis Papanagnou on Sun Dec 4 15:50:39 2022
    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:
    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:
    [...]

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Janis Papanagnou@21:1/5 to applemcg on Mon Dec 5 04:32:06 2022
    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

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From castAway@21:1/5 to All on Mon Dec 5 01:40:28 2022
    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

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From castAway@21:1/5 to applemcg on Mon Dec 5 01:42:36 2022
    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

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Janis Papanagnou@21:1/5 to castAway on Mon Dec 5 09:06:04 2022
    On 05.12.2022 05:42, castAway wrote:
    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

    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

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Jalen Q@21:1/5 to Janis Papanagnou on Sun Mar 5 16:54:47 2023
    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

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)