#!/bin/bash
# Desc: Displays `uptime` information but with ISO-8601 time period
#   string.

yell() { echo "$0: $*" >&2; } # Yell, Die, Try Three-Fingered Claw technique; # Ref/Attrib: https://stackoverflow.com/a/25515370
die() { yell "$*"; exit 111; }
try() { "$@" || die "cannot $*"; }
timeDuration(){
    # Desc: Given seconds, output ISO-8601 duration string
    # Ref/Attrib: ISO-8601:2004(E), §4.4.4.2 Representations of time intervals by duration and context information
    # Note: "1 month" ("P1M") is assumed to be "30 days" (see ISO-8601:2004(E), §2.2.1.2)
    # Usage: timeDuration [1:seconds] ([2:precision])
    # Version: 1.0.5
    # Input: arg1: seconds as base 10 integer >= 0  (ex: 3601)
    #        arg2: precision level (optional; default=2)
    # Output: stdout: ISO-8601 duration string (ex: "P1H1S", "P2Y10M15DT10H30M20S")
    #         exit code 0: success
    #         exit code 1: error_input
    #         exit code 2: error_unknown
    # Example: 'timeDuration 111111 3' yields 'P1DT6H51M'
    # Depends: date 8, bash 5, yell,
    local argSeconds argPrecision precision returnState remainder
    local fullYears fullMonths fullDays fullHours fullMinutes fullSeconds
    local hasYears hasMonths hasDays hasHours hasMinutes hasSeconds
    local witherPrecision output
    local displayYears displayMonths displayDays displayHours displayMinutes displaySeconds
    
    argSeconds="$1"; # read arg1 (seconds)
    argPrecision="$2"; # read arg2 (precision)
    precision=2; # set default precision

    # Check that between one and two arguments is supplied
    if ! { [[ $# -ge 1 ]] && [[ $# -le 2 ]]; }; then
	yell "ERROR:Invalid number of arguments:$# . Exiting.";
	returnState="error_input"; fi

    # Check that argSeconds provided
    if [[ $# -ge 1 ]]; then
	## Check that argSeconds is a positive integer
	if [[ "$argSeconds" =~ ^[[:digit:]]+$ ]]; then
   	    :
	else
	    yell "ERROR:argSeconds not a digit.";
	    returnState="error_input";
	fi
    else
	yell "ERROR:No argument provided. Exiting.";
	exit 1;
    fi

    # Consider whether argPrecision was provided
    if  [[ $# -eq 2 ]]; then
	# Check that argPrecision is a positive integer
	if [[ "$argPrecision" =~ ^[[:digit:]]+$ ]] && [[ "$argPrecision" -gt 0 ]]; then
	precision="$argPrecision";
	else
	    yell "ERROR:argPrecision not a positive integer. (is $argPrecision ). Leaving early.";
	    returnState="error_input";
	fi;
    else
	:
    fi;
    
    remainder="$argSeconds" ; # seconds
    ## Calculate full years Y, update remainder
    fullYears=$(( remainder / (365*24*60*60) ));
    remainder=$(( remainder - (fullYears*365*24*60*60) ));
    ## Calculate full months M, update remainder
    fullMonths=$(( remainder / (30*24*60*60) ));
    remainder=$(( remainder - (fullMonths*30*24*60*60) ));
    ## Calculate full days D, update remainder
    fullDays=$(( remainder / (24*60*60) ));
    remainder=$(( remainder - (fullDays*24*60*60) ));
    ## Calculate full hours H, update remainder
    fullHours=$(( remainder / (60*60) ));
    remainder=$(( remainder - (fullHours*60*60) ));
    ## Calculate full minutes M, update remainder
    fullMinutes=$(( remainder / (60) ));
    remainder=$(( remainder - (fullMinutes*60) ));
    ## Calculate full seconds S, update remainder
    fullSeconds=$(( remainder / (1) ));
    remainder=$(( remainder - (remainder*1) ));
    ## Check which fields filled
    if [[ $fullYears -gt 0 ]]; then hasYears="true"; else hasYears="false"; fi
    if [[ $fullMonths -gt 0 ]]; then hasMonths="true"; else hasMonths="false"; fi
    if [[ $fullDays -gt 0 ]]; then hasDays="true"; else hasDays="false"; fi
    if [[ $fullHours -gt 0 ]]; then hasHours="true"; else hasHours="false"; fi
    if [[ $fullMinutes -gt 0 ]]; then hasMinutes="true"; else hasMinutes="false"; fi
    if [[ $fullSeconds -ge 0 ]]; then hasSeconds="true"; else hasSeconds="false"; fi
    
    ## Determine which fields to display (see ISO-8601:2004 §4.4.3.2)
    witherPrecision="false"
    
    ### Years
    if $hasYears && [[ $precision -gt 0 ]]; then
	displayYears="true";
	witherPrecision="true";
    else
	displayYears="false";
    fi;
    if $witherPrecision; then ((precision--)); fi;
    
    ### Months
    if $hasMonths && [[ $precision -gt 0 ]]; then
	displayMonths="true";
	witherPrecision="true";
    else
	displayMonths="false";
    fi;
    if $witherPrecision && [[ $precision -gt 0 ]]; then
	displayMonths="true";
    fi;
    if $witherPrecision; then ((precision--)); fi;

    ### Days
    if $hasDays && [[ $precision -gt 0 ]]; then
	displayDays="true";
	witherPrecision="true";
    else
	displayDays="false";
    fi;
    if $witherPrecision && [[ $precision -gt 0 ]]; then
	displayDays="true";
    fi;
    if $witherPrecision; then ((precision--)); fi;

    ### Hours
    if $hasHours && [[ $precision -gt 0 ]]; then
	displayHours="true";
	witherPrecision="true";
    else
	displayHours="false";
    fi;
    if $witherPrecision && [[ $precision -gt 0 ]]; then
	displayHours="true";
    fi;
    if $witherPrecision; then ((precision--)); fi;

    ### Minutes
    if $hasMinutes && [[ $precision -gt 0 ]]; then
	displayMinutes="true";
	witherPrecision="true";
    else
	displayMinutes="false";
    fi;
    if $witherPrecision && [[ $precision -gt 0 ]]; then
	displayMinutes="true";
    fi;
    if $witherPrecision; then ((precision--)); fi;

    ### Seconds

    if $hasSeconds && [[ $precision -gt 0 ]]; then
	displaySeconds="true";
	witherPrecision="true";
    else
	displaySeconds="false";
    fi;
    if $witherPrecision && [[ $precision -gt 0 ]]; then
	displaySeconds="true";
    fi;
    if $witherPrecision; then ((precision--)); fi;

    ## Determine whether or not the "T" separator is needed to separate date and time elements
    if ( $displayHours || $displayMinutes || $displaySeconds); then
	displayDateTime="true"; else displayDateTime="false"; fi
    
    ## Construct duration output string
    output="P"
    if $displayYears; then
	output=$output$fullYears"Y"; fi
    if $displayMonths; then
	output=$output$fullMonths"M"; fi
    if $displayDays; then
	output=$output$fullDays"D"; fi
    if $displayDateTime; then
	output=$output"T"; fi
    if $displayHours; then
	output=$output$fullHours"H"; fi
    if $displayMinutes; then
	output=$output$fullMinutes"M"; fi
    if $displaySeconds; then
	output=$output$fullSeconds"S"; fi

    ## Output duration string to stdout
    echo "$output" && returnState="true";

    #===Determine function return code===
    if [ "$returnState" = "true" ]; then
	return 0;
    elif [ "$returnState" = "error_input" ]; then
	yell "ERROR:input";
	return 1;
    else
	yell "ERROR:Unknown";
	return 2;
    fi

} # Get duration (ex: PT10M4S )
uptimeIso8601() {
    # Desc: Get system uptime with ISO_8601-formatted time period
    # Usage: uptimeIso8601    
    # Output: A string similar to `uptime` but with date and uptime
    #   time period replaced with a period/endtime ISO-8601-formatted
    #   string.
    #     ex: `P3DT23H36M46S/2021-03-28T14:47:48+0000,  5 users,  load average: 0.09, 0.10, 0.13`
    # Note: No arguments used.
    # Depends: timeDuration() v1.0.5, uptime
    # Version: 0.0.1
    # Ref/Attrib: get uptime in seconds https://leo.leung.xyz/wiki/Linux_Uptime_in_Seconds
    sysUptime=$(</proc/uptime);
    sysUptime=${sysUptime%%.*}; # 
    sysUptimeIso="$(timeDuration $sysUptime 4)";
    timeNow="$(date +%Y-%m-%dT%H:%M:%S%z)";
    users_load="$(uptime | cut -d',' -f3-)";
    output="$sysUptimeIso"/"$timeNow","$users_load ";
    echo "$output";
} # Get uptime with ISO-8601 time period
main() {
    uptimeIso8601;
}

main;
# Author: Steven Baltakatei Sandoval
# License: GPLv3+