Skip to content

Commit

Permalink
feat: add new script uxt ⌚️
Browse files Browse the repository at this point in the history
  • Loading branch information
oldratlee committed Jan 30, 2024
1 parent e7ccb63 commit 9583fb0
Show file tree
Hide file tree
Showing 2 changed files with 278 additions and 0 deletions.
219 changes: 219 additions & 0 deletions bin/uxt
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
#!/usr/bin/env bash
# @Function
# Convert unix time to human readable date string.
# Note: The range of the 10-digit unix time in second include recent date.
# 1000000000: 2001-09-09 01:46:40 +0000
# 9999999999: 2286-11-20 17:46:39 +0000
#
# @Usage
# $ uxt 0 # date 1970-01-01 00:00:00 GMT
# 1970-01-01 08:00:00 +0800
# # for large integer, default auto treat first 10 digits as second
# $ uxt 1234567890 # unix time of second
# 2009-02-14 07:31:30 +0800
# $ uxt 1234567890333 # unix time of milliseconds(10 + 3 digits)
# 2009-02-14 07:31:30.333 +0800
# $ uxt 12345678903 # unix time of 10 + 1 digits
# 2009-02-14 07:31:30.3 +0800
# # support multiply arguments
# $ uxt 0 1234567890 12345678903
#
# @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/shell.md#-unix-time
# @author Jerry Lee (oldratlee at gmail dot com)
set -eEuo pipefail

PROG=$(basename -- "$0")
readonly PROG
readonly PROG_VERSION='2.6.0-dev'

################################################################################
# util functions
################################################################################

red_print() {
# if stdout is a terminal, turn on color output.
# '-t' check: is a terminal?
# check isatty in bash https://stackoverflow.com/questions/10022323
if [ -t 1 ]; then
printf "\e[1;31m%s\e[0m\n" "$*"
else
printf '%s\n' "$*"
fi
}

is_integer() {
[[ "$1" =~ ^-?[[:digit:]]+$ ]]
}

die() {
red_print "Error: $*" >&2
exit 1
}

usage() {
local -r exit_code=${1:-0}
(($# > 0)) && shift
local -r out=$(((exit_code != 0) + 1))

# NOTE: $'foo' is the escape sequence syntax of bash
(($# > 0)) && red_print "$*"$'\n' >&"$out"

cat >&"$out" <<EOF
Usage: $PROG [OPTION] unix-time [unix-time...]
Convert unix time to human readable date string.
Note: The range of the 10-digit unix time in second include recent date:
1000000000: 2001-09-09 01:46:40 +0000
9999999999: 2286-11-20 17:46:39 +0000
Example:
# for large integer, default auto treat first 10 digits as second
$ $PROG 1234567890 # unix time of second
2009-02-14 07:31:30 +0800
$ $PROG 1234567890333 # unix time of milliseconds(10 + 3 digits)
2009-02-14 07:31:30.333 +0800
$ $PROG 12345678903 # unix time of 10 + 1 digits
2009-02-14 07:31:30.3 +0800
# support multiply arguments
$ $PROG 0 1234567890 12345678903
Options:
-u, --time-unit set the time unit of given epochs
-Z, --no-time-zone do not print time zone
-t, --trim-decimal-tailing-0
trim the tailing zeros of second decimal
-h, --help display this help and exit
-V, --version display version information and exit
EOF

exit "$exit_code"
}

progVersion() {
printf '%s\n' "$PROG $PROG_VERSION"
exit
}

################################################################################
# parse options
################################################################################

declare -a args=()
unit=
trim_decimal_tailing_0=false
no_tz=false
while (($# > 0)); do
case "$1" in
-u | --unit)
unit=$2
shift 2
;;
-Z | --no-time-zone)
no_tz=true
shift
;;
-t | --trim-decimal-tailing-0)
trim_decimal_tailing_0=true
shift
;;
-h | --help)
usage
;;
-V | --version)
progVersion
;;
--)
shift
args=(${args[@]:+"${args[@]}"} "$@")
break
;;
-*)
usage 2 "Error: unrecognized option '$1'"
;;
*)
args=(${args[@]:+"${args[@]}"} "$1")
shift
;;
esac
done

[[ -z $unit || $unit =~ ^(s|second|ms|millisecond)$ ]] ||
usage 1 "illegal time unit '$unit'! support values: 'second'/'s', 'millisecond'/'ms'"
[[ $unit = second ]] && unit=s
[[ $unit = millisecond ]] && unit=ms

readonly args unit trim_decimal_tailing_0 no_tz

if [ ${#args[@]} = 0 ]; then
usage 1 'No argument provided!' >&2
fi

################################################################################
# biz logic
################################################################################

for a in "${args[@]}"; do
is_integer "$a" || die "argument $a is not integer!"
done

readonly MIN_DATE_SEC=-67768040609769943 MAX_DATE_SEC=67768036191647999

print_date() {
local -r input=$1
# split input integer to sign and number part
local -r sign_part=${input%%[!-]*} # remove digits from tail
local -r number_part=${input#-} # remove sign from head
local -r np_len=${#number_part} # length of number part

local second_part=0 decimal_part=
# case 1: is unix time in second?
if [[ $unit = s ]]; then
second_part=$number_part
else
# case 2: is unix time in millisecond?
if [[ $unit = ms ]]; then
if ((np_len > 3)); then
second_part=${number_part:0:np_len-3}
decimal_part=${number_part:np_len-3:3}
else
printf -v decimal_part '%03d' "$number_part"
fi
# case 3: auto detect by length
else
# <= 10 digits, treat as second
if ((np_len <= 10)); then
second_part=$number_part
# for long integer(> 10 digits), treat first 10 digits as second,
# and the rest as decimal/nano second(almost 9 digits)
else
((np_len <= 19)) || die "argument $input contains $np_len digits(>19), too many to treat as a recent date(first 10-digits as seconds, rest at most 9 digits as decimal)"
second_part=${number_part:0:10}
decimal_part=${number_part:10:9}
fi
fi

# trim tailing zeros of decimal
if $trim_decimal_tailing_0; then
while true; do
local len_decimal=${#decimal_part}
decimal_part=${decimal_part%0}
((${#decimal_part} < len_decimal)) || break
done
fi
fi
local -r second_part decimal_part

local -r seconds_value=$sign_part$second_part
((${#second_part} <= 17 || (seconds_value >= MIN_DATE_SEC && seconds_value <= MAX_DATE_SEC))) ||
die "argument $input(seconds: $seconds_value${decimal_part:+, decimal_part: .$decimal_part}) is out of range! seconds should be in range [$MIN_DATE_SEC, $MAX_DATE_SEC]."

local date_input=$seconds_value${decimal_part:+.$decimal_part}
local format_n=${decimal_part:+.%${#decimal_part}N}
local format_tz=' %z'
$no_tz && format_tz=
date -d "@$date_input" +"%Y-%m-%d %H:%M:%S$format_n$format_tz"
}

for a in "${args[@]}"; do
print_date "$a"
done
59 changes: 59 additions & 0 deletions test-cases/uxt_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#!/usr/bin/env bash
set -eEuo pipefail

READLINK_CMD=readlink
if command -v greadlink &>/dev/null; then
READLINK_CMD=greadlink
fi

BASE=$(dirname -- "$($READLINK_CMD -f -- "${BASH_SOURCE[0]}")")
cd "$BASE"

#################################################
# commons and test data
#################################################

readonly uxt="../bin/uxt"

#################################################
# test cases
#################################################

test_utx_auto_detect() {
assertEquals "2024-01-30 00:00:00 +0000" "$(TZ=0 "$uxt" 1706572800)"
assertEquals "1900-01-30 00:00:00 +0000" "$(TZ=0 "$uxt" -- -2206483200)"

assertEquals "1970-01-01 00:00:00 +0000" "$(TZ=0 "$uxt" 0)"
assertEquals "1970-01-01 00:01:40 +0000" "$(TZ=0 "$uxt" -- 100)"
assertEquals "1969-12-31 23:58:20 +0000" "$(TZ=0 "$uxt" -- -100)"

# shellcheck disable=SC2016
assertFalse 'should fail, 20 more than 19 digits' '"$uxt" 12345678901234567890'
}

test_utx_unit_second() {
assertEquals "2024-01-30 00:00:00 +0000" "$(TZ=0 "$uxt" -u s 1706572800)"
assertEquals "1900-01-30 00:00:00 +0000" "$(TZ=0 "$uxt" -u s -- -2206483200)"

assertEquals "1970-01-01 00:00:00 +0000" "$(TZ=0 "$uxt" -u s 0)"
assertEquals "1970-01-01 00:01:40 +0000" "$(TZ=0 "$uxt" -u s -- 100)"
assertEquals "1969-12-31 23:58:20 +0000" "$(TZ=0 "$uxt" -u s -- -100)"

# shellcheck disable=SC2016
assertFalse 'should fail, 20 more than 19 digits' '"$uxt" -u s 12345678901234567890'
}

test_utx_unit_ms() {
assertEquals "2024-01-30 00:00:00.000 +0000" "$(TZ=0 "$uxt" -u ms 1706572800000)"
assertEquals "1900-01-30 00:00:00.000 +0000" "$(TZ=0 "$uxt" -u ms -- -2206483200000)"

assertEquals "1970-01-01 00:00:00.000 +0000" "$(TZ=0 "$uxt" -u ms 0)"
assertEquals "1970-01-01 00:01:40.000 +0000" "$(TZ=0 "$uxt" -u ms -- 100000)"
assertEquals "1969-12-31 23:58:20.000 +0000" "$(TZ=0 "$uxt" -u ms -- -100000)"
}

#################################################
# Load and run shUnit2.
#################################################

source "$BASE/shunit2-lib/shunit2"

0 comments on commit 9583fb0

Please sign in to comment.