#!/usr/bin/nawk -f
# SccsId[] = "@(#)holidays.awk 1.25 05/29/13 (AWK holiday calculation/reporting program)"
#----------------------------------------------------------------------#
# holidays.awk #
# -------------------------------------------------------------------- #
# #
# Copyright (c) 2003-2013 by Bob Orlando. All rights reserved. #
# #
# Permission to use, copy, modify and distribute this software #
# and its documentation for any purpose and without fee is hereby #
# granted, provided that the above copyright notice appear in all #
# copies, and that both the copyright notice and this permission #
# notice appear in supporting documentation, and that the name of #
# Bob Orlando not be used in advertising or publicity pertaining #
# to distribution of the software without specific, written prior #
# permission. Bob Orlando makes no representations about the #
# suitability of this software for any purpose. It is provided #
# "as is" without express or implied warranty. #
# #
# BOB ORLANDO DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS #
# SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY #
# AND FITNESS. IN NO EVENT SHALL BOB ORLANDO BE LIABLE FOR ANY #
# SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES #
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER #
# IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, #
# ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF #
# THIS SOFTWARE. #
# #
# -------------------------------------------------------------------- #
# Program documentation and notes located at the bottom of script. #
#----------------------------------------------------------------------#
BEGIN \
{
progname = "holidays.awk"
"date +\%Y\%m\%d" | getline Ymd # Escapes (\) in "\%Y\%m" prevents %Y
close("date +\%Y\%m\%d") # from looking like percentYpercent
yyyy = substr(Ymd,1,4) # (an SCCS keyword)
mm = substr(Ymd,5,2)
today = substr(Ymd,7,2)
yyyymm = yyyy""mm
opt_B = 0
opt_b = 0
opt_d = 0
opt_l = 0
opt_m = 0
opt_s = 0
opt_t = 0
mutex = 0 # Mutually exclusive options (bdls)
error = 0
#--------------------------------------------------------------#
# Make 0-based weekdays, and 1-based month and julian arrays. #
#--------------------------------------------------------------#
weekdayn["Sun"] = 0; weekdays[0] = "Sun"
weekdayn["Mon"] = 1; weekdays[1] = "Mon"
weekdayn["Tue"] = 2; weekdays[2] = "Tue"
weekdayn["Wed"] = 3; weekdays[3] = "Wed"
weekdayn["Thu"] = 4; weekdays[4] = "Thu"
weekdayn["Fri"] = 5; weekdays[5] = "Fri"
weekdayn["Sat"] = 6; weekdays[6] = "Sat"
split("January February March " \
"April May June " \
"July August September " \
"October November December", month_names)
split("0 31 59 90 120 151 181 212 243 273 304 334 365", julian_days)
split("0 31 60 91 121 152 182 213 244 274 305 335 366", julian_leap)
split("31 28 31 30 31 30 31 31 30 31 30 31" , month_days )
bizdays[0] = 0 # Initialize
#--------------------------------------------------------------#
# Validate and process options and optargs. #
#--------------------------------------------------------------#
if (ARGV[1] == "-H") show_documentation(progname)
if (ARGV[1] == "-h") exit_usage()
#-------------------------------------#
for (n=1; n<ARGC; n++) # -y = yyyy year (mostly testing) #
{ #-------------------------------------#
if (ARGV[n] == "-y")
{
yyyy = ARGV[n+1]
if (yyyy !~ /^[0-2][0-9][0-9][0-9]$/)
exit_usage("Bogus year ("yyyy") in -y optarg!")
yyyy = sprintf("%04d",yyyy)
yyyymm = yyyy""mm # Use default month
ARGV[n] = ARGV[n+1] = ""
break
}
} #-------------------------------------#
for (n=1; n<ARGC; n++) # -m = mm month (mostly testing) #
{ #-------------------------------------#
if (ARGV[n] == "-m")
{
mm = ARGV[n+1]
if (mm !~ /^[01]?[0-9]$/)
exit_usage("Bogus month number ("mm") in -m optarg!")
if (mm < 1 || mm > 12)
exit_usage("Invalid month number ("mm") in -m optarg!")
mm = sprintf("%02d",mm)
yyyymm = yyyy""mm
ARGV[n] = ARGV[n+1] = ""
break
}
} #-------------------------------------#
for (n=1; n<ARGC; n++) # -B = Business day today? (ret 0|1) #
{ #-------------------------------------#
if (ARGV[n] == "-B")
{
opt_B = 1; mutex++; ARGV[n] = ""; break
}
} #-------------------------------------#
for (n=1; n<ARGC; n++) # -b = Business day (ret yyyymmdd) #
{ #-------------------------------------#
if (ARGV[n] == "-b")
{
opt_b = ARGV[n+1] # Make opt_b the business day
mutex++
if (opt_b !~ /^(-?[0-2]?[0-9]|last)$/)
exit_usage("Bogus business day ("opt_b") in -b optarg!")
sub(/^-0+$/,"last",opt_b) # In case we get "-0".
ARGV[n] = ARGV[n+1] = ""
break
}
} #-------------------------------------#
for (n=1; n<ARGC; n++) # -l = Date list only (ret multiline) #
{ #-------------------------------------#
if (ARGV[n] == "-l")
{
opt_l = 1; mutex++; ARGV[n] = ""; break
}
} #-------------------------------------#
for (n=1; n<ARGC; n++) # -s = Date list only (ret one line) #
{ #-------------------------------------#
if (ARGV[n] == "-s")
{
opt_s = 1; mutex++; ARGV[n] = ""; break
}
} #-------------------------------------#
for (n=1; n<ARGC; n++) # -t = Compare with today's date #
{ #-------------------------------------#
if (ARGV[n] == "-t")
{
opt_t = 1; ARGV[n] = ""; break
}
} #-------------------------------------#
for (n=1; n<ARGC; n++) # -d = nth weekday (ret yyyymmdd) #
{ #-------------------------------------#
if (ARGV[n] == "-d")
{
mutex++
nth = weekday = optarg = ARGV[n+1]
a_cnt = split(optarg,WWOA,".") # WWOA = Nth, Weekday, On/After
nth = WWOA[1]
weekday = WWOA[2]
on_or_after = WWOA[3]
if (nth !~ /^(-?[0-2]?[0-9])|(last)$/)
exit_usage("Bogus nth day ("nth") in -d optarg ("opt_d")!")
sub(/^-0+$/,"last",nth) # And just in case we get "-0".
if (weekday !~ /^(Sun|Mon|Tue|Wed|Thu|Fri|Sat|[0-6])$/)
exit_usage("Bogus weekday ("weekday") in -d optarg ("opt_d")!")
if (weekday !~ /^[0-6]$/)
weekday = weekdayn[weekday]
if (a_cnt >= 3)
{
if (on_or_after !~ /^[0-1]?[0-9]?$/)
exit_usage("Bogus on_or_after ("on_or_after") in -d optarg ("opt_d")!")
}
else
on_or_after = 1
if (on_or_after >= (month_days[mm+0] - 7))
exit_usage("You're kidding, right? " \
month_names[mm+0] " only has "month_days[mm+0]"!")
yyyymmdd = holiday_date(yyyy,mm,nth,weekday,on_or_after)
opt_d = 1
exit # Go to END section (skip reading any holiday file).
}
} #-----------------------------------#
for (n=1; n<ARGC; n++) # Any other option is invalid. #
{ #-----------------------------------#
if (ARGV[n] ~ /^-/)
exit_usage("Invalid option, '"ARGV[n]"'.")
}
shift_ARGV() # Drops nulled (blank) ARGV variables
#----------------------------------------------------------#
# If the user failed to supply a holidays fileid, push the #
# following as an argument (and hope the file exists). #
#----------------------------------------------------------#
if (ARGC < 2)
{
ARGV[1] = "/usr/local/bin/holidays" # Default to this.
ARGC = 2
}
} # BEGIN {}
#======================================================================#
# M A I N #
#======================================================================#
{
sub(/\043.*/ ,"") # Trash everything after pound sign.
sub(/^[\t ]+/,"") # Remove leading and
sub(/[\t ]+$/,"") # trailing whitespace
if ($0 ~ /^$/) next # Skip essentially blank lines.
#------------------------------------------------------------#
# Need New Year holiday for next year for Dec. last bizday. #
#------------------------------------------------------------#
if ($1 ~ /^0?1$/ && $2 ~ /^0?1$/)
{
new_years_day = 1
next_yyyy = yyyy + 1
next_yyyymmdd = sprintf("%4d%02d%02d",next_yyyy,$1,$2)
}
else
{
new_years_day = 0
}
on_or_after = 1
#------------------------------------------------------------#
# Process month and day. #
#------------------------------------------------------------#
if ($1 ~ /^[01]?[0-9]$/ && $2 ~ /^0?[1-5]\.0?[0-6]\.?0?[0-9]?$/)
{ # m m nth.weekday.on_or_after
split($2,WWOA,".") # WWOA = Occurrence, Weekday, On/After
nth = WWOA[1]
weekday = WWOA[2]
on_or_after = WWOA[3]
if (on_or_after == "") on_or_after = 1
if (on_or_after >= (month_days[$1+0] - 7))
{
exit_usage("You're kidding, right? " \
month_names[$1+0] " only has "month_days[$1+0]"!")
}
yyyymmdd = holiday_date(yyyy,$1,nth,weekday,on_or_after)
$1 = $2 = "" # Null these args
}
else if ($1 ~ /^[01]?[0-9]$/ && $2 ~ /^last\.0?[0-6]$/)
{ # m m last.weekday.on_or_after
nth = weekday = $2
sub(/\..*/ ,"",nth )
sub(/^.*\./,"",weekday)
yyyymmdd = holiday_date(yyyy,$1,nth,weekday)
$1 = $2 = "" # Null these args
}
else if ($1 ~ /^[01]?[0-9]$/ && $2 ~ /^[0-3]?[0-9]$/)
{ # m m d d
yyyymmdd = sprintf("%4d%02d%02d",yyyy,$1,$2)
$1 = $2 = "" # Null these args
}
#------------------------------------------------------------#
# If we have an adjustment by itself (i.e. for the day after #
# Thanksgiving Day, then calculate that date. #
#------------------------------------------------------------#
if ($3 ~ /^[+\-][0-9]+$/)
{
yyyymmdd = dateplus(yyyymmdd,$3)
#----------------------------#
# Do the same for New Year. #
#----------------------------#
if (new_years_day == 1)
next_yyyymmdd = dateplus(next_yyyymmdd,$3)
$3 = "" # Null this arg
}
#------------------------------------------------------------#
# If we have a pattern that looks like [0-6][+\-][0-9]+ then #
# test yyyymmdd's weekday to see if it matches DoW target. #
# If it does, adjust yyyymmdd by the +|- "adj" value and #
# recalculate the date. #
#------------------------------------------------------------#
if ($3 ~ /0?[0-6]?[+\-][0-9][0-9]?/)
{ # target +|- adj
day = adj = $3
sub(/[+\-].*/,"" ,day)
sub(/[+\-].*/,":&",adj) # Negative value returns future date.
sub(/^.*:/ ,"" ,adj) # Positive returns past date.
$3 = "" # Null this arg
if (day_of_week(yyyymmdd) == day)
yyyymmdd = dateplus(yyyymmdd,adj)
#----------------------------#
# Do the same for New Year. #
#----------------------------#
if (new_years_day == 1)
if (day_of_week(next_yyyymmdd) == day)
next_yyyymmdd = dateplus(next_yyyymmdd,adj)
}
#------------------------------------------------------------#
# Remove leading whitespace from $0 and store the pertinents #
# into our hash array (we'll print it in the END section). #
#------------------------------------------------------------#
sub(/^[\t ]+/,"" ,$0) # Leading whitespace
gsub(/[\t ]+/,"_",$0) # Change intervening whitespace to underscores
if (holiday_hash[yyyymmdd] ~ /^$/)
holiday_hash[yyyymmdd] = weekdays[day_of_week(yyyymmdd)]":"$0
if (new_years_day == 1)
if (holiday_hash[next_yyyymmdd] ~ /^$/)
holiday_hash[next_yyyymmdd] = weekdays[day_of_week(next_yyyymmdd)]":"$0
} # End of M A I N
END \
{
if (error > 0) exit error
if (mutex > 1)
print "Options B, b, d, l, and s are mutually exclusive! ",
"We\047ll give it our best shot." | "cat 1>&2"
if (opt_d) # Weekday date: give it and adios.
{ #--------------------------------------------------#
# If testing for "today" and today is the desired #
# weekday, then exit 1 (true), printing nothing. #
#--------------------------------------------------#
if (opt_t)
{
if (yyyymmdd == Ymd) exit 1
else exit 0
}
print yyyymmdd
exit substr(yyyymmdd,7,2)
}
holidays = ""
#------------------------------------------------------------#
# Since we already have our holidays in a hash, this is an #
# excellent place to get the business day. #
#------------------------------------------------------------#
if (opt_B) # See if today a business day?
{
if (Ymd in holiday_hash) exit 0 # False if found in holiday hash.
else exit 1 # Else True.
}
if (opt_b) # Specific business day
{
yyyy = substr(yyyymm,1,4)
if (opt_m) mm = sprintf("%02d",opt_m)
else mm = substr(yyyymm,5,2)
#---------------------------------------------#
# Zero in on just the holidays for this month #
#---------------------------------------------#
for (holiday in holiday_hash)
if (match(holiday,yyyy""mm))
holidays = holidays" "holiday # Combine into a scalar list.
if (opt_b ~ /last|^-[0-2]?[0-9]$/)
rev_bizday=1
else
rev_bizday=0
mm_days = month_days[mm+0]
for (i=j=1; i<=mm_days; i++)
{
dd = sprintf("%02d",i)
dow = day_of_week(yyyy,mm,dd)
#-----------------------------------------------------------#
# If day of week ! Sat or Sun and it's not a holiday, then #
# if the day (i) is the same as the day we want (opt_b), #
# print out the yyyymmdd date and exit with the month day. #
# Else increment the business day counter (j) and continue. #
#-----------------------------------------------------------#
if (! (dow ~ /[06]/ || match(holidays," "yyyy""mm""dd)))
{
if (rev_bizday)
{
if (i <= mm_days)
{
bizdays[j] = dd # Push the date into bizdays array.
bizdays[0] = j
j++
}
else
{
break
}
}
else if (j == opt_b)
{ #------------------------------------------------------#
# If testing for "today" and today is the desired #
# business day, then exit 1 (true), printing nothing. #
#------------------------------------------------------#
if (opt_t > 0)
{
if (yyyy""mm""dd == Ymd) exit 1
else exit 0
}
else
{
print yyyy""mm""dd
}
exit i
}
else
{
bizdays[0] = j
j++
}
} # if (dow !~ /[06]/ && ! match(holidays," "yyyy""mm""dd))
} # for (i=j=1; i<month_days[mm+0]; i++)
#--------------------------------------------------------------#
# If we want business day counting backward from EOM, do this. #
#--------------------------------------------------------------#
if (rev_bizday)
{
if (opt_b == "last")
{
if (opt_t > 0)
if (yyyy""mm""bizdays[bizdays[0]] == Ymd) exit 1
else exit 0
print yyyy""mm""bizdays[bizdays[0]]
exit dd
}
opt_b = opt_b - opt_b - opt_b # Reverse/remove the sign
if (opt_b < bizdays[0])
{
if (opt_t > 0)
if (yyyy""mm""(bizdays[bizdays[0] - opt_b]) == Ymd) exit 1
else exit 0
print yyyy""mm""bizdays[bizdays[0] - opt_b]
exit bizdays[bizdays[0] - opt_b]
}
else if (opt_b == bizdays[0])
{
if (opt_t > 0)
if (yyyy""mm""dd == Ymd) exit 1
else exit 0
print yyyy""mm""bizdays[1]
exit bizdays[1]
}
} # if (rev_bizday)
exit_usage("You're kidding, right? " \
month_names[mm+0] " only has "bizdays[0]" business days!")
exit 0
} # if (opt_b) # Business day
#--------------------------------------------------------------------#
# Display holidays saved in holiday_hash[]. #
#--------------------------------------------------------------------#
for (holiday in holiday_hash)
{
if (match(holiday,"^"yyyy))
{
if (opt_s)
{
holidays = holidays" "holiday # Combine into a scalar.
sub(/^ +/,"",holidays) # Trim leading blank
}
else if (opt_l)
print holiday
else
print holiday":"holiday_hash[holiday]
}
}
if (opt_s)
print holidays
exit 0
} # END {}
#======================================================================#
# U S E R F U N C T I O N S #
# (in alphabetical order) #
#----------------------------------------------------------------------#
function basedate( \
YYYY,MM,DD, \
yyyy,cc,ddddd,leapdays,cc_leapdays,accum_days \
) #
# -------------------------------------------------------------------- #
# Globals: julian_days[] and julian_leap[] #
# Calls: leap_year() #
#----------------------------------------------------------------------#
{
#------------------------------------------------------#
# First, decrement the year and get the number of days #
# for all the years preceeding it (in the Common Era). #
#------------------------------------------------------#
yyyy = YYYY
if ((yyyy - 1) > 0)
{
yyyy--
cc = int(yyyy / 100)
ddddd = yyyy * 365
leapdays = int(yyyy / 4)
cc_leapdays = (cc > 0) ? (int(cc / 4)) : 0
ddddd = ddddd + leapdays - cc + cc_leapdays
yyyy++
}
ddddd += DD
#-------------------------------------------------------#
# Add the previous month YTD days to our ddddd. If it #
# is a leap year, assign julian_leap[] to accum_days[]. #
# Then add in YTD days for previous months of this year #
#-------------------------------------------------------#
if (leap_year(yyyy))
for (i=0; i<13; i++) accum_days[i] = julian_leap[i]
else
for (i=0; i<13; i++) accum_days[i] = julian_days[i]
ddddd += accum_days[MM += 0] # 'MM += 0' removes leading zero
return (ddddd)
} # function base_date
#----------------------------------------------------------------------#
function calculate_date(YYYY,MM,DD,ADJ, adj,yyyymmdd) #
# -------------------------------------------------------------------- #
# Calls: leap_year(), ddddd_to_yyyymmdd(), and basedate() #
#----------------------------------------------------------------------#
{
adj = ADJ
if (adj ~ /\\055/)
{
sub(/\\055/,"",adj)
yyyymmdd = ddddd_to_yyyymmdd(basedate(YYYY,MM,DD) - adj)
}
else
{
sub(/\\053/,"",adj)
yyyymmdd = ddddd_to_yyyymmdd(basedate(YYYY,MM,DD) + adj)
}
return yyyymmdd
} # function calculate_date()
#----------------------------------------------------------------------#
function dateplus(YYYYMMDD,ADJ, mm,dd,yyyy,mmddyy) #
# -------------------------------------------------------------------- #
# Globals: ARGV[], ARGC, julian_days[], julian_leap[] #
# Calls: validate_yyyymmdd(), calculate_date() #
# Exits on error. #
#----------------------------------------------------------------------#
{
fname = "dateplus"
#------------------------------------------------------------#
# If last arg looks like a valid date, parse, validate, and #
# use it, else, use the current yyyymmdd. #
#------------------------------------------------------------#
if (YYYYMMDD ~ /^[0-2][0-9][0-9][0-9][0-1][0-9][0-3][0-9]$/)
{
mm = substr(YYYYMMDD,5,2)
dd = substr(YYYYMMDD,7,2)
yyyy = substr(YYYYMMDD,1,4)
validate_yyyymmdd(yyyy,mm,dd)
}
#------------------------------------------------------------#
# Positive (or unsigned) integer for adj days next? #
#------------------------------------------------------------#
if (ADJ !~ /^[\+\-]?[0-9]+$/)
{
print fname()" adjustment value ("ADJ")",
"not [+|-]numeric!" | "cat 1>&2"
exit 1
}
#------------------------------------------------------------#
# Everything looks good, calculate/return new yyyymmdd date. #
#------------------------------------------------------------#
return calculate_date(yyyy,mm,dd,ADJ)
} # function dateplus
#----------------------------------------------------------------------#
function day_of_week(YYYY,MM,DD, adj,yyyy,mm,dow) #
#----------------------------------------------------------------------#
{
if (length(YYYY) == 8) # User may call us with yyyymmdd
{ # instead of yyyy,mm,dd
DD = substr(YYYY,7,2)
MM = substr(YYYY,5,2)
YYYY = substr(YYYY,1,4)
}
adj = int((14 - MM) / 12)
yyyy = YYYY - adj
mm = MM + (12 * adj) - 2
dow = (DD + yyyy \
+ int(yyyy/4) \
- int(yyyy/100) \
+ int(yyyy/400) \
+ int(31 * mm / 12) \
) % 7
return dow
} # function day_of_week()
#----------------------------------------------------------------------#
function ddddd_to_yyyymmdd( \
DDDDD, \
ddddd,yyyy,cc,cc_leapdays,leapdays, \
mmdd,julian,i \
) #
# -------------------------------------------------------------------- #
# Globals: julian_days[] and julian_leap[] #
#----------------------------------------------------------------------#
{
#----------------------------------------------------------#
# Reduce DDDDD arg to values in yyyy and ddddd variables. #
#----------------------------------------------------------#
for (i=1; ; i++)
{
yyyy = int(DDDDD / 365) - i
ddddd = DDDDD - yyyy * 365
cc = int(yyyy / 100)
leapdays = int(yyyy / 4)
cc_leapdays = (cc > 0) ? (int(cc / 4)) : 0
leapdays = leapdays - cc + cc_leapdays
ddddd -= leapdays
yyyy++
if (ddddd > 0) break
}
if (leap_year(yyyy))
for (i in julian_leap)
julian[i] = julian_leap[i]
else
for (i in julian_days)
julian[i] = julian_days[i]
#----------------------------------------------------------#
# Reduce ddddd to month and day (we already have the year) #
#----------------------------------------------------------#
for (i=13; i>0; i--)
{
if (julian[i] < ddddd)
{
ddddd -= julian[i]
mmdd = sprintf("%02d%02d",i,ddddd)
break
}
}
return (yyyy""mmdd)
} # function ddddd_to_yyyymmdd
#----------------------------------------------------------------------#
function exit_usage(ERRMSG, opts) # Global vars: progname, error #
#----------------------------------------------------------------------#
{
error = 5
if (ERRMSG != "") print "\n"ERRMSG
opts="-- -B [-b nn] [-d nn.Www.OnOrA ] -lHhst [-y yyyy] [-m mm]"
print "\nUsage: [gn]awk -f "prog" "opts" holidayfile" ,
"\n -- = Allows passing script opts and args." ,
"\n -B = Return true if today is a business" ,
"\n day." ,
"\n -b nn = Return nn business day yyyymmdd date" ,
"\n (nn may also be \047last\047 for the" ,
"\n last business day of the month, and" ,
"\n -n (minus n) may also be used for the",
"\n nth business day from end of month." ,
"\n -d n.Www[.OoA] = Return nth weekday (Www = Sun-Sat," ,
"\n n.w[.OoA] and the alternate, \047.w\047 takes" ,
"\n 0-6). Optionally, an on-or-after" ,
"\n (.OoA) day of the month may also be" ,
"\n provided." ,
"\n -H = Full documentation (functions only" ,
"\n when the current working directory" ,
"\n is also the program directory)." ,
"\n -h = Summary help (Usage)." ,
"\n -l = Return multiline yyyymmdd date list." ,
"\n -s = Return a line of yyyymmdd\047s." ,
"\n -t = Test resultant date against today" ,
"\n (works with -b and -d options)." ,
"\n -y yyyy = Use \047yyyy\047 as the year." ,
"\n -m mm = Use the mm that follows as the month" ,
"\n (for business day calculations)." ,
"\n holidayfile = Calculation directives file (not used",
"\n with \047-d\047 option).\n" ,
"\n "progname" terminated.\n"
exit error
} # function exit_usage
#----------------------------------------------------------------------#
function holiday_date( \
YYYY,MM,NTH,DOW,ON_OR_AFTER, \
fname,usage,wk,day,dd,mm_days,status,nth,dow \
)
# -------------------------------------------------------------------- #
# Globals: month_names[] #
#----------------------------------------------------------------------#
{
fname = "holiday_date"
usage = "Usage: "fname"(yyyy,mm, occurrence|'last', day_of_week)\n"
#----------------------------------------------------------------#
# Validate incoming args (we can skip yyyy here). #
#----------------------------------------------------------------#
if (! (MM ~ /^[0-1]?[0-9]$/ || MM < 1 || MM > 12))
{
print fname"(): Invalid month '"MM"'!\n" | "cat 1>&2"
exit 2
}
if (! (NTH ~ /^0?[1-5]$/ || NTH ~ /^last$/))
{
print fname"(): Invalid occurrence, '"NTH"'!\n" | "cat 1>&2"
exit 3
}
if (DOW !~ /^0?[0-6]$/)
{
print fname"(): Invalid Day of week '"DOW"'!\n" | "cat 1>&2"
exit 4
}
mm = MM + 0 # Assign and lose any leading zero.
n = (NTH ~ /^[0-9]$/) ? NTH + 0 : 5
day = DOW + 0 # Weekday (0=Sunday, 1=Monday, etc.).
dd = ""
status = ""
nth = ""
#----------------------------------------------------------------#
# Passed validation, return the day of the month. If user wants #
# the last N-day, then get the number of days for that month and #
# calculate the day using it. #
#----------------------------------------------------------------#
if (mm ~ /^(1|3|5|7|8|10|12)$/) mm_days = 31
else if (mm ~ /^(4|6|9|11)$/) mm_days = 30
else mm_days = leap_year(YYYY) ? 29 : 28
#----------------------------------------------------------------#
# If EOM day of the week is Sunday (0), then use 7 so the modulo #
# operation (% 7) will work as expected (it doesn't seem to like #
# negatives--sooooooooo sensative :-)) #
# -------------------------------------------------------------- #
# 20130529.091642: Updates subsequent to this 2003-07-04 (v1.3) #
# needed test, makes the following two (now commented out) lines #
# #
# dow = day_of_week(YYYY,MM,mm_days)
# if (dow == 0) dow = 7
# #
# ... completely unnecessary. #
#----------------------------------------------------------------#
#----------------------------------------------------------------#
# In case last occurrence is wanted, we back up a week at a #
# time to see if we can find the day within the number of days #
# allowed in our month. #
#----------------------------------------------------------------#
for (; n>0; n--) # n already initialized.
{
dd = (day - day_of_week(YYYY,MM,ON_OR_AFTER)) % 7
if (dd < 0) dd += 7
dd = (7 * n) - 6 + (dd % 7) + (ON_OR_AFTER - 1)
if (dd <= mm_days) break
}
if (dd > mm_days)
{
if (n ~ /1$/) nth = n"st"
else if (n ~ /2$/) nth = n"nd"
else if (n ~ /3$/) nth = n"rd"
else nth = n"th"
#--------------------------------------------------#
# Don't sweat this for business day calculations. #
#--------------------------------------------------#
if (! opt_b)
{
print "The "nth" occurrence of day "day" ("dd") exceeds",
month_names[mm]"\'s "mm_days" days!\n"
return 99
}
}
return YYYY""MM""sprintf("%02d",dd)
} # function holiday_date
#----------------------------------------------------------------------#
function leap_year(YYYY) # YYYY = year. Returns true|false (1|0). #
#----------------------------------------------------------------------#
{
return (((YYYY%4 == 0 && YYYY%100 != 0) || (YYYY%400 == 0)) ? 1 : 0)
} # function leap_year
#----------------------------------------------------------------------#
function shift_ARGV(i,j,k) #
#----------------------------------------------------------------------#
{
k = ARGC
for (i=1; i<=ARGC; i++)
{
if (ARGV[i] == "") # If the argument is empty, see is we
{ # can shift the next one down to it.
for (j=i+1; j<=k; j++)
{
if (ARGV[j] == "") # If the next one is empty, try the one
continue # following that.
ARGV[i] = ARGV[j] # Once we find an argument, move it down,
ARGV[j] = "" # null where it was, decrement arg counter,
break # can do this with the next argument.
}
}
}
for (i=1; i<k; i++) # Adjust ARGC
if (ARGV[i] == "") # If the argument is empty,
ARGC-- # decrement ARGC
return ARGC
} # function shift_ARGV
#----------------------------------------------------------------------#
function show_documentation(MOI, n,line) #
#----------------------------------------------------------------------#
{
n = 0
while (getline <MOI > 0) # Searching ourselves for the doc'n section.
{
#------------------------------------------#
# Until we find the documentation section, #
# keep looking at each line. #
#------------------------------------------#
if (n == 0)
{
if ($0 ~ /^\043 +D O C U M E N T A T I O N/)
{
n = 1
print line
print $0
}
else
{
line = $0
}
continue
}
else print # Once we find it, print until EOF.
} # while (getline <MOI > 0)
if (n == 0) # Means we did not find documentation section.
{
print "NO DOCUMENTATION section found for "MOI" in the",
"current directory (cwd).\nTry again after first",
"changing to the program directory." | "cat 1>&2"
exit 1 # Exit failure
}
exit 1 # Exit failure anyway because we don'y live for this.
} # function show_documentation
#----------------------------------------------------------------------#
function validate_yyyymmdd( \
YYYY,MM,DD, \
yyyy,mm,dd,mm_days \
) #
# -------------------------------------------------------------------- #
# Globals: month_names[] #
# Calls: leap_year() #
# Exits on error. #
#----------------------------------------------------------------------#
{
yyyy = YYYY
mm = MM
dd = DD
mm += 0
if (mm==1||mm==3||mm==5||mm==7||mm==8||mm==10||mm==12)
mm_days=31
else if (mm==4||mm==6||mm==9||mm==11)
mm_days=30
else if (mm==2)
mm_days = (leap_year(yyyy)) ? 29 : 28
else
{
printf("Month %02d is invalid.\n",mm) | "cat 1>&2"
exit 6
}
if (dd > mm_days)
{
printf("%s, %d does not have %d days.\n",
month_names[mm],yyyy,dd) | "cat 1>&2"
exit 7
}
return 0
} # function validate_yyyymmdd
#======================================================================#
# D O C U M E N T A T I O N #
#======================================================================#
# #
# Author: Bob Orlando (Bob@OrlandoKuntao.com) #
# #
# Date: February 1, 2003 #
# #
# Program ID: holidays.awk #
# #
# Usage: [gn]awk -f holidays.awk -- -B [-b nn] [-d n.Www.OoA] \ #
# -lHhst [-y yyyy] [-m mm] holidayfile #
# #
# nawk|gawk = Program runs with either. (See "Usage #
# Note:" below for "#!" operation.) #
# -- = Allows passing script opts and args. #
# -B = Return true if today is a business #
# day. #
# -b nn = Return nn business day as yyyymmdd #
# (nn may also be specified as "last" #
# for that business day of the month, #
# or -n (minus n) for nth business day #
# from end of the month). #
# -d n.Www.OoA = Return nth weekday (Www = Sun-Sat, #
# and "n" may also be given as "last" #
# for the last "Www" day of the month). #
# -d n.w.OoA = Alternate suntax for the above only #
# the ".w" is 0-6 for Sun-Sat. The #
# ".OoA" is an optional on-or-after #
# day of the month that says the date #
# wanted must be on or after the OoA'th #
# day of the month. #
# -H = Full formal documentation (functions #
# only when the current working #
# directory is the program directory). #
# -h = Summary help (Usage). #
# -l = Return multiline yyyymmdd date list. #
# -s = Return a single line of yyyymmdd's. #
# -t = Test resultant date against today #
# (works with -b and -d options). #
# -y yyyy = Use yyyy for the year. #
# -m mm = Use the mm that follows as the month #
# (for business day calculations). #
# holidayfile = Calculation directives file (neither #
# used nor needed with "-d" option). #
# The file lays out as follows: #
# #
# #
# Example: awk -f /home/awk/holidays.awk -- -y 2013 \ #
# /home/awk/SocialSecurity.txt \ #
# | sed -n 's/^\(....\)\(..\)\(..\)\(.*\)/\2-\3/p' \ #
# | sort #
# #
# Using SocialSecurity.txt for 'holidayfile' to return the #
# 4th Wednesday of every month ... #
# #
# > #------------------------------------------------------------# #
# > # Mm N.Day Adj Holiday name # Comments # #
# > # -- ----- --- --------------------------- ----------------- # #
# > 01 4.3 4th Wednesday # 4th Wed in Jan #
# > 02 4.3 4th Wednesday # 4th Wed in Feb #
# > 03 4.3 4th Wednesday # 4th Wed in Mar #
# > 04 4.3 4th Wednesday # 4th Wed in Apr #
# > 05 4.3 4th Wednesday # 4th Wed in May #
# > 06 4.3 4th Wednesday # 4th Wed in Jun #
# > 07 4.3 4th Wednesday # 4th Wed in Jul #
# > 08 4.3 4th Wednesday # 4th Wed in Aug #
# > 09 4.3 4th Wednesday # 4th Wed in Sep #
# > 10 4.3 4th Wednesday # 4th Wed in Oct #
# > 11 4.3 4th Wednesday # 4th Wed in Nov #
# > 12 4.3 4th Wednesday # 4th Wed in Dec #
# #
# the following output was generated: #
# #
# 01-23 #
# 02-27 #
# 03-27 #
# 04-24 #
# 05-22 #
# 06-26 #
# 07-24 #
# 08-28 #
# 09-25 #
# 10-23 #
# 11-27 #
# 12-25 #
# #
# #
# Data File: holidays #
# #
# > #------------------------------------------------------------# #
# > # * DO NOT REMOVE * DO NOT REMOVE * DO NOT REMOVE * # #
# > #------------------------------------------------------------# #
# > # This file contains values that allow any program to # #
# > # calculate holidays using either fixed month and day or by # #
# > # specifying an occurrence in a month like the 4th Thursday # #
# > # in November or "last" Monday in May. Adjustments also are # #
# > # permitted for those fixed holiday dates that fall on # #
# > # weekends but are observed either the Friday before or the # #
# > # Monday after, as well as days like the Friday after the # #
# > # Thanksgiving Day observance (U.S.). # #
# > # # #
# > # Leading whitespace is ignored, as is everything following # #
# > # and including the octothorpe (#-sign). # #
# > # # #
# > # Mm N.D.OnOrA Adj Holiday name # Comments # #
# > # -- --------- --- ----------------------- ----------------- # #
# > 01 1 New Year's # M-F #
# > 01 1 6-1 New Year's (pre-obs) # Sat? Use Fri #
# > # 01 1 6+2 New Year's (post-obs) # Sat? Or Monday #
# > 01 1 0+1 New Year's (post-obs) # Sun? Use Mon #
# > 01 3.1 M.L.King Jr. Birthday # 3rd Mon in Jan #
# > 05 last.1 Memorial Day # Last Mon in May #
# > 07 4 Independence # M-F #
# > 07 4 6-1 Independence (pre-obs) # Sat? Use Fri #
# > 07 4 0+1 Independence (post-obs) # Sun? Use Mon #
# > 09 1.1 Labor Day # 1st Mon in Sep #
# > 11 4.4 Thanksgiving (US) # 4th Thu in Nov #
# > 11 4.4 +1 Thanksgiving Day II # Fri after #
# > 12 25 Christmas # M-F #
# > 12 25 6-1 Christmas (pre-obs) # Sat? Use Fri #
# > 12 25 0+1 Christmas (post-obs) # Sun? Use Mon #
# > #------------------------------------------------------------# #
# > # Unique dates. # #
# > #------------------------------------------------------------# #
# > # 04 1.0.6 Falklands ST Beg # 1st Sun on/after Apr 6 #
# > # 09 1.0.8 Falklands DST Beg # 1st Sun on/after Sep 8 #
# #
# #
# Usage Note: If your OS recognizes the "#!" (shebang) syntax, #
# you can place a "#!/usr/bin/nawk -f" (or gawk) #
# at the start of this program (as I have) thereby #
# allowing you skip the "[gn]awk -f" during invocation. #
# #
# #
# Purpose: Return the year's holiday dates and names as calculated #
# from the values in a holidays file. A good description #
# of what this program does is provided in the holiday #
# file layout just below. Optionally (-b), return the #
# yyyymmdd date for the nth business day of the month. #
# #
# #
# Description: Although I have Perl modules that do the same thing, I #
# wanted an awk program to provide the same information #
# so I could use it in my shell scripts. #
# #
# The program outputs holidays in three forms: #
# #
# 1. Dates (yyyymmdd) only with all dates on a #
# single line. #
# 2. Dates only in a multiline list. #
# 3. yyyymmdd:Day:Holiday_name_as_in_holidays_file #
# #
# The user can parse or assign the output as needed #
# via any number of shell script or other language tools. #
# #
# #
# ------ Here is an example of one such shell script ------ #
# #
# > #!/bin/sh -vx #
# > bin=/usr/local/bin #
# > holidays_awk=$bin/holidays.awk #
# > holiday_file=$bin/holidays #
# > holiday_cmd="nawk -f $holidays_awk --" #
# > #
# > HOLIDAYS=`$holiday_cmd -s $holiday_file`; export HOLIDAYS #
# > #
# > if [ ."`echo $HOLIDAYS | grep \`date +\%Y\%m\%d\``" != . ] #
# > then #
# > echo "Today is a holiday. Hoo-rah! :-)" #
# > fi #
# > #
# > #------------------------------------------------------# #
# > # Display holidays for this year. # #
# > #------------------------------------------------------# #
# > $holiday_cmd $holiday_file | sort \ k -- $holiday_file` #
# > | nawk -v today="$today" \ #
# > '{ #
# > gsub(/:/," ",$0) #
# > gsub(/_/," ",$0) #
# > if (match($0,today)) #
# > $0 = $0"\t **HOLIDAY**" #
# > print $0 #
# > }' #
# > #
# > #------------------------------------------------------# #
# > # Business day reporting: 1st, next-to-last, and last. # #
# > #------------------------------------------------------# #
# > bizday=`$holiday_cmd -b 1 $holiday_file` #
# > if [ `date +\%Y\%m\%d` = $bizday ]; then #
# > echo "Today is the first business day of the month." #
# > fi #
# > #
# > bizday=`$holiday_cmd -b -1 $holiday_file` #
# > if [ `date +\%Y\%m\%d` = $bizday ]; then #
# > echo "Today is the next-to-last business day." #
# > fi #
# > #
# > bizday=`$holiday_cmd -b last $holiday_file` #
# > if [ `date +\%Y\%m\%d` = $bizday ]; then #
# > echo "Today is the last business day of the month." #
# > fi #
# > #
# > #------------------------------------------------------# #
# > # See if today is a business day at all. # #
# > #------------------------------------------------------# #
# > $holiday_cmd -B $holiday_file #
# > if [ $? -eq 1 ]; then #
# > echo "Today is a business day--get crackin'." #
# > else #
# > echo "Today is NOT a business day--have fun." #
# > fi #
# > #
# > #------------------------------------------------------# #
# > # First Monday and last Sunday of the current month. # #
# > #------------------------------------------------------# #
# > fst_monday=`$holiday_cmd -d 1.Mon` #
# > fst_monday=`$holiday_cmd -d 1.1` # Alternative syntax #
# > lst_sunday=`$holiday_cmd -d last.Sun` #
# > lst_sunday=`$holiday_cmd -d last.0` # Alternative syntax #
# > #
# > #------------------------------------------------------# #
# > # Falklands Day Light Savings Dates. # #
# > #------------------------------------------------------# #
# > Falk_DST=`$holiday_cmd -d 1.0.8 -m 9` # DST begins #
# > Falk_ST=` $holiday_cmd -d 1.0.6 -m 4` # Return to ST #
# > #
# > #------------------------------------------------------# #
# > # See if today is the 1st Monday of the month and # #
# > # if it is the first business day of the month (the # #
# > # business day example uses the default holidays file, # #
# > # /usr/local/bin/holidays (assuming the file exists). # #
# > #------------------------------------------------------# #
# > $holiday_cmd -t -d 1.Mon || echo "Today is 1st Monday" #
# > $holiday_cmd -t -b 1 || echo "Today is 1st Business day" #
# > #
# > #------------------------------------------------------# #
# > # Find the date range wherein the 5th bizday falls- # #
# > # --great for setting cron date range, like this. # #
# > # # #
# > # 00 01 5-7 * * `/usr/local/bin/holidays.awk -- \ # #
# > # -b 5 -t` || some_process > some_process.out 2>&1 # #
# > # # #
# > # Here again, by not specifying a holidays file, # #
# > # we use the default, /usr/local/bin/holidays. # #
# > #------------------------------------------------------# #
# > years="00 01 02 03 04 05 06 07 08 09" #
# > years="$years 10 11 12 13 14 15 16 17 18 19" #
# > years="$years 20 21 22 23 24 25" #
# > for n in $years #
# > do #
# > /usr/local/bin/holidays.awk -- -y 20$n -b 5 #
# > done #
# #
# #
# Credits: 1. The algorithms used here are from Marcos J. Montes #
# (http://www.smart.net/~mmontes/ushols.html#ALG). #
# Montes, in turn, credits Claus Tondering's work #
# (http://www.tondering.dk/claus/calendar.html), #
# for the algorithm and Timothy Barmann and Bobby #
# Cossum for their contributions in simplifying the #
# equations used. Those gentlemen did the really #
# hard work. #
# #
# 2. The On-or-After (OoA) capability available in the #
# holidays file and with the -d option was requested #
# by Kevin Marshall who also provided a critical part #
# of the code to accomplish this. This he did so he #
# could determine, for example, daylight saving begin #
# and end dates for the Falkland islands. #
# #
# #
# Exit Codes: For all except business day (-b) and weekday (-d) #
# options... #
# #
# Zero = Normal | Success #
# Nonzero = Abnormal | Failure #
# #
# With business day (-b) and weekday (-d) options ... #
# #
# Nonzero = The day of the month on which the weekday #
# weekday business day falls. #
# Zero = Abnormal | Failure #
# #
# With business day (-b) and weekday (-d) options used #
# in conjunction with "today is" (-t) ... #
# #
# One |true = The weekday or business day IS today. #
# Zero|false = The day of the month on which the #
# weekday or business day falls #
# is NOT today. #
# #
# In addition to descriptive error messages (something #
# rare in AWK) each error exit has a different status #
# to assist in locating the source of the error. #
# #
# #
# Notes: 1. I could have called my dateplus.c to provide the #
# same functionality as dateplus() and day_of_week(), #
# but my intent was to have a single, self-contained #
# piece of code. #
# #
# 2. I am no mathematician. Heck, I'm not that great #
# an Awk programmer, so there are doubtless, many #
# places where this code can be improved/simplified. #
# Feel free, therefore, to forward comments and #
# suggestions to Bob@OrlandoKuntao.com. That said, #
# my preference is for readable/understandable code #
# over more efficient, but difficult to follow (read #
# obfuscated) code since readable code (even if less #
# efficient) makes maintenance much less a chore. #
# #
# #
# Modified: 2013-05-29 Bob Orlando #
# v1.25 * Commented out two lines that were part of #
# v1.03 modification to holiday_date function #
# because subsequent updates to that same #
# function made those two lines completely #
# unnecessary. #
# #
# 2011-12-02 Bob Orlando #
# v1.24 * Added usage "Example:" to documentation. #
# #
# 2005-10-25 Bob Orlando #
# v1.23 * Add an "on or after" extension used in our #
# holidays file to the -d option (i.e. #
# -d "N.D.OnOrA") allowing the user to specify #
# an occurrence or week number (N) and week #
# day (D) within that week, AND also indicate #
# the date must fall on or after a given day #
# of the month (OnOrA). #
# #
#----------------------------------------------------------------------#
|
|
©Copyright Bob Orlando, 2003-2016 All rights reserved. |
http://www.OrlandoKuntao.com E-mail: Bob@OrlandoKuntao.com |
Last update:
Feb. 2, 2016 by Bob Orlando |