/
dateh
executable file
·193 lines (172 loc) · 6.19 KB
/
dateh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
#!/usr/bin/env bash
VERSION=1.1
short_ordinals=(invalid 1st 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th
12th 13th 14th 15th 16th 17th 18th 19th 20th 21st 22nd 23rd 24th 25th
26th 27th 28th 29th 30th 31st)
long_ordinals=(invalid first second third fourth fifth sixth seventh
eighth ninth tenth eleventh twelfth thirteenth fourteenth fifteenth
sixteenth seventeenth eighteenth nineteenth twentieth twenty-first
twenty-second twenty-third twenty-fourth twenty-fifth twenty-sixth
twenty-seventh twenty-eighth twenty-ninth thirtieth thirty-first)
# human_date [<date command options>]
human_date() {
local date_args=("${@:1:$(($# - 1))}") date_format="${*: -1}"
local h_date h_DATE h_week h_month h_year
# "Freeze" user & current times with GNU date, while adding
# some intermediate results in user time for later use
local date_obj now_obj
date_obj="$(date "${date_args[@]}" "${date_format}[%Y-%-m-%-d-%a-%A-%s-%::z|${DATEH_DEFAULT_FORMAT:-%Y-%m-%d}]")"
now_obj="$(date +%Y-%-m-%s-%::z)"
# Now parse the resulting objects
if [[ "$date_obj" =~ (.*)\[([0-9]+)-([0-9]+)-([0-9]+)-(.*)-(.*)-([0-9]+)-([+-][0-9]+:[0-9]+:[0-9]+)\|(.*)\] ]]; then
local date_out="${BASH_REMATCH[1]}" date_year="${BASH_REMATCH[2]}"
local date_month="${BASH_REMATCH[3]}" date_dom="${BASH_REMATCH[4]}"
local date_dow="${BASH_REMATCH[5]}" date_DOW="${BASH_REMATCH[6]}"
local date_utc_secs="${BASH_REMATCH[7]}" date_tz="${BASH_REMATCH[8]}"
local h_dateplus="${BASH_REMATCH[9]}"
else
echo "FATAL ERROR: Date object '$date_obj' can't be parsed." >&2
return 1
fi
if [[ "$now_obj" =~ ([0-9]+)-([0-9]+)-([0-9]+)-([+-][0-9]+:[0-9]+:[0-9]+) ]]; then
local now_year="${BASH_REMATCH[1]}" now_month="${BASH_REMATCH[2]}"
local now_utc_secs="${BASH_REMATCH[3]}" now_tz="${BASH_REMATCH[4]}"
else
echo "FATAL ERROR: Now object '$now_obj' can't be parsed." >&2
return 1
fi
# Now derive epoch equivalent of local timestamps, otherwise
# relative days math will only be correct in UTC+0 timezone
local date_tzsecs date_secs now_tzsecs now_secs
date_tzsecs="$(tzsecs "$date_tz")"
date_secs=$((date_utc_secs + date_tzsecs))
now_tzsecs="$(tzsecs "$now_tz")"
now_secs=$((now_utc_secs + now_tzsecs))
### ORDINAL DAYS OF MONTH
date_out="${date_out//@\{o\}/${short_ordinals[${date_dom}]}}"
date_out="${date_out//@\{O\}/${long_ordinals[${date_dom}]}}"
### RELATIVE DAYS
local date_days=$((date_secs / 86400)) now_days=$((now_secs / 86400))
# Now let's look at the difference in days
case $(( date_days - now_days )) in
-[2-7])
h_date="last $date_dow"
h_DATE="last $date_DOW"
;;
-1)
h_date="yesterday"
;;
0)
h_date="today"
;;
1)
h_date="tomorrow"
;;
[2-7])
h_date="next $date_dow"
h_DATE="next $date_DOW"
;;
*)
h_date="$(relative_interval day $((date_days - now_days)))"
;;
esac
date_out="${date_out//@\{d\}/${h_date}}"
date_out="${date_out//@\{d+\}/${h_dateplus:-${h_date}}}"
date_out="${date_out//@\{D\}/${h_DATE:-${h_date}}}"
### RELATIVE WEEKS
# Weeks are counted from Unix epoch (midnight 1970-01-01),
# with midnight 1970-01-05 (Mon) being the start of week 1
local date_weeks=$(((date_days + 3) / 7)) now_weeks=$(((now_days + 3) / 7))
h_week="$(relative_interval week $((date_weeks - now_weeks)))"
date_out="${date_out//@\{w\}/${h_week}}"
### RELATIVE MONTHS & YEARS
h_month="$(relative_interval month $(((date_year * 12 + date_month) - (now_year * 12 + now_month))))"
h_year="$(relative_interval year $((date_year - now_year)))"
date_out="${date_out//@\{m\}/${h_month}}"
date_out="${date_out//@\{y\}/${h_year}}"
### AUTO-SELECT
if [[ "$h_year" == [1-9]* ]]; then
h_auto="$h_year"
elif [[ "$h_month" == [1-9]* ]]; then
h_auto="$h_month"
elif [[ "$h_week" == [1-9]* ]]; then
h_auto="$h_week"
else
h_auto="$h_date"
h_AUTO="$h_DATE"
fi
date_out="${date_out//@\{h\}/${h_auto}}"
date_out="${date_out//@\{H\}/${h_AUTO:-${h_auto}}}"
# And we're done
echo "${date_out}"
}
# relative_interval <week|month|year> <interval_offset>
relative_interval() {
case "$2" in
-1) echo "last ${1}";;
0) echo "this ${1}";;
1) echo "next ${1}";;
*) [[ $2 -lt 0 ]] && echo "${2#-} ${1}s ago" || echo "${2} ${1}s' time";;
esac
}
# tzsecs <[+-]HH:MM:SS>
tzsecs() {
if [[ "$1" =~ ([+-])([0-9]{2}):([0-9]{2}):([0-9]{2}) ]]; then
echo "${BASH_REMATCH[1]#+}$(( ${BASH_REMATCH[2]#0} * 3600 + ${BASH_REMATCH[3]#0} * 60 + ${BASH_REMATCH[4]#0} ))"
else
echo "ERROR: Unable to parse TZ spec '$1'." >&2
echo 0
fi
}
usage() {
cat <<EOF
USAGE: $0 [OPTION]... [+FORMAT]
or: $0 [-u|--utc|--universal] [MMDDhhmm[[CC]YY][.ss]]
Display the current time in the given FORMAT, or set the system date.
NOTE: This replicates GNU date functionality, and adds the following
format sequences:
@{d} relative date w/ abbrev day name (e.g. yesterday, next Fri, 17 days ago)
@{D} relative date w/ full day name (e.g. next Friday, 17 days' time)
@{d+} like @{d}, unless ref date exceeds now +/- 1 week, then use
\$DATEH_DEFAULT_FORMAT instead (default: $DATEH_DEFAULT_FORMAT))
@{w} relative week (e.g. last week, 3 weeks' time)
@{m} relative month (e.g. last month, 3 months' time)
@{y} relative year (e.g. last year, 3 years' time)
@{h} auto-select relative representation (abbreviated day name)
@{H} auto-select relative representation (full day name)
@{o} short ordinal day of month (e.g. 1st, 25th)
@{O} long ordinal day of month (e.g. first, twenty-fifth)
Examples:
$0
$0 -d "now" "+@{D} %X"
$0 -d "now + 3 weeks" "+the @{O} of %B, %Y"
DATEH_DEFAULT_FORMAT=%m/%d/%Y $0 -d "last month" \\
"+@{d+}, @{w}, @{m}, @{y} %H:%M %P"
EOF
if [[ $1 == long ]]; then
cat <<EOF
..... GNU date help follows .....
$(date --help)
EOF
else
cat <<EOF
See date(1) man page or \`$0 --longhelp\` for GNU date format sequences.
EOF
fi
exit 0
}
while true; do
case "$1" in
-h|--help) usage;;
-H|--longhelp) usage long;;
-V|--version) echo "dateh ${VERSION}"; exit 0;;
*) break;;
esac
shift
done
if [[ $# -eq 0 || "${*: -1}" != +*\@\{*\}* ]]; then
# No human format specifier, just call "date" as normal
date "$@"
else
human_date "$@"
fi