The macosxhints Forums

The macosxhints Forums (http://hintsforums.macworld.com/index.php)
-   UNIX - Newcomers (http://hintsforums.macworld.com/forumdisplay.php?f=15)
-   -   crontab(s) for the last "work-day" of every month (http://hintsforums.macworld.com/showthread.php?t=34624)

Hal Itosis 02-07-2005 09:59 AM

crontab(s) for the last "work-day" of every month
 
Getting the _first_ day of the month is too easy:
Code:

#min        hour        mday        month        wday        command
15        9        1        *        *        /usr/local/bin/doSomething
#runs at 9:15 am on the first day of every month (on any day).

But if the Mac is only turned on Mon-Fri, the script is missed.

Limiting it to weekdays only is also simple:
Code:

#min        hour        mday        month        wday        command
15        9        1        *        1-5        /usr/local/bin/doSomething
#runs at 9:15 am on the first day of some months (only on weekdays).

But if the 1st of the month falls on Sat or Sun, the script is skipped.

--

Anyway, those two examples are for the beginning of a month.
What is the trick to set a crontab which will run only on the
last WEEKday at the *end* of EVERY month? If that's just not
possible, then how about the first weekday of every month...
I'm guessing it'll take more than one crontab entry, but i can't
figure out how to limit it to just **one** run per month.

-HI-

[edit: after looking at this page, something tells me cron can't do it]

voldenuit 02-07-2005 11:00 AM

If you absolutely need to do that, you could come up with a script checking for the existence of a file such as /tmp/mmyy and if it does not exist, do its thing, then touch that file.

Then call your cron the first 3 day of a month, it will run only the one time the file does not yet exist as the last bit of payload of your script is to create it.

dmacks 02-07-2005 12:12 PM

Yeah, just using cron scheduling, this is too complex a set of criteria. OTOH, it's pretty easy to write an "if (first weekday of a month) {do something}" script (i.e., no run_once semaphore file needed), and use cron to run it first three days of each month. Perl has date manipulation routines in core, or also the excellent Date::Manip module. Or heck, pick a language, and:
Code:

# cron this days 1-3 of a month
# will run do_it on first weekday of each month
if ( (date=1 and day=weekday) or day=monday ) then do_it


Hal Itosis 02-07-2005 04:58 PM

Almost got it.
February is the tricky part.
This version runs twice in Feb (sometimes):
Code:

#min        hour        mday        month                        wday        command
15        9        31        1,3,5,7,8,10,12                1-5        /
15        9        30        4,6,9,11                1-5        /
15        9        29        2                        1-5        /
15        9        28        2                        1-5        /

--

Yeah, some external 'computing' is probably the easiest,
but this is fun. (Almost like homework or a bonus quiz).

:D

FORGET IT... doesn't cover enough ground.
(I must be having a flashback or something). :eek:

forbin 02-07-2005 07:46 PM

The trick is to use cal ... $6 would be the last friday .. 1-7 = Sun-Sat ...

for i in `cal | awk '{print $6}'`; do
if [ "x" != "x"$i ]; then
day=$i
fi
done

today=`date | awk '{print $3}'`

if [ $day = $today ]; then
'do want you want ...'
fi
exit

themacnut 02-08-2005 06:46 AM

I know many of you are finding this to be a fun puzzle, and I'm sure the OP has good reasons for only having his/her Mac running on weekdays-but wouldn't it be easier to just leave the Mac on 7 days a week? You can turn off the monitor, or have it sleep-that solves dealing with the most power-intensive part of the system...

Hal Itosis 02-08-2005 12:17 PM

forbin:

Thanks for the snippet. Time for me to study up on cal and awk.
(all I know about 'awk' is that the k stands for Kernighan) :o



themacnut:

Wouldn't it have been "easier" for you to not post that obvious solution? ;)

forbin 02-08-2005 01:01 PM

Quote:

Originally Posted by Hal Itosis
Time for me to study up on cal and awk.

The awk in this case is just taking the 6th field .. Try 'cal' in terminal .. basically, it just shows you a monthly calender view:

February 2005
Sun Mon Tue Wed Thu Fri Sat
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


So .. the script just gets the last entry for field 6 (which is Friday the 25th)

Hal Itosis 02-08-2005 02:02 PM

I think we both fell into the same trap...
(the "last workday" for Feb is Mon 28th)


Now let's look at next month:
Code:

$ cal 3 2005

    March 2005
 S  M Tu  W Th  F  S
      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

Thursday the 31st is the last workday for the month of March. :eek:

That's what I seek. Believe me, I'm googling like mad. Tools that do
this seem to cost money. The nearest free solution I've found so far
(to a similar problem) is: Script help - find third workday of month.

--

This looks like a simple solution (I know even less about perl):
Quote:


"How can I calculate the last business day (payday!) of a month?
Start from the end of the month and then work backwards until we reach a weekday.
Code:

  my $dt = DateTime->last_day_of_month( year => 2003, month => 8 );

  # day 6 is Saturday, day 7 is Sunday
  while ( $dt->day_of_week >= 6 ) { $dt->subtract( days => 1 ) }

  print "Payday is ", $dt->ymd, "\n";

This isn't the absolute most efficient solution, but it's easy to understand."

bramley 02-08-2005 07:17 PM

Here's a one-liner with call to Applescript

Code:

osascript -e '(((day of ((current date) + 1 * days)) = 1) and not (((weekday of (current date)) = Wednesday) or ((weekday of (current date)) = Sunday))) as integer'
gives a '1' if it is the last working day - a '0' if it's not.

[EDIT - Whoops! Doesn't work if the last day of the month is on a weekend]

Hal Itosis 02-09-2005 09:34 AM

Quote:

Originally Posted by bramley
EDIT - Whoops! Doesn't work if the last day of the month is on a weekend

I see you caught it. Looks like months ending on Wednesday are
also a problem. (I'll post my prepared reply as written anyway):

Wow bramley!

Nice... that is one condensed piece of code!
Sorry but I have to say it may need a tweak.
Unable to deduce its inner logic, I was forced
to write an AppleScript with which to put your
function through its paces. The results were:

It works correctly for the months of:
Jan, Feb, Mar, May, Jun, Sep and Oct.

But, errors happen in:
April: Friday 29 returns 0 -- Saturday 30 returns 1
July: Friday 29 returns 0 -- no July run!!!
August: Wednesday 31 returns 0 -- no August run!!!
November: Wednesday 30 returns 0 -- no November run!!!
December: Friday 30 returns 0 -- Saturday 31 returns 1

In case you (or anyone) would like to give it another go,
here is the script I used to examine your code's output:
Code:

set runDatesList to {}
set startDate to date "Friday, December 31, 2004 9:00:00 AM"

repeat with i from 1 to 366
        set thisDate to startDate + (i * days)
        if my testForMonthsLastWorkday(thisDate) is 1 then
                copy thisDate to the end of runDatesList
                if (weekday of thisDate) is Sunday or ¬
                        (weekday of thisDate) is Saturday then
                        copy "<==ERROR" to the end of runDatesList
                end if
                copy return to the end of runDatesList
        end if
end repeat
return runDatesList

on testForMonthsLastWorkday(cd) -------------------------------------------
        return (((day of (cd + 1 * days)) = 1) and not (((weekday of ¬
                cd) = Wednesday) or ((weekday of cd) = Sunday))) as integer

end testForMonthsLastWorkday ----------------------------------------------

...where the blue text is your code (and 'cd' replaces 'current date' for test purposes).

bramley 02-09-2005 10:02 AM

The error in the previously posted AS was to include 'Wednesday' instead of 'Saturday' (I was using Wednesday to test the script):

Code:

return (((day of (cd + 1 * days)) = 1) and not (((weekday of ¬
                cd) = Saturday) or ((weekday of cd) = Sunday))) as integer

Unfortunately I posted the wrong bit of script. With the above correction, the script doesn't notice July or December, but gets everything else (for 2005 anyway) Will think some more.

bramley 02-09-2005 01:05 PM

The following in a shell script will give a '1' if today is the last working day of the current month:

Code:

osascript -e '(((day of ((current date) + 1 * days) = 1) and not ((weekday of (current date) = Saturday) or (weekday of (current date) = Sunday))) or ((weekday of (current date) = Friday) and (day of ((current date) + 3 * days) < 4))) as integer'
Replacing the test routine in HI's test script with the following should give the last working days for 2005.

Code:

on testForMonthsLastWorkday(cd) -------------------------------------------
        return (((day of (cd + 1 * days) = 1) and not ((weekday of cd = Saturday) or (weekday of cd = Sunday))) or ((weekday of cd = Friday) and (day of (cd + 2 * days) < 3))) as integer
end testForMonthsLastWorkday ----------------------------------------------


Hal Itosis 02-10-2005 10:03 AM

BRILLIANT BRAMLEY!

Not only are you a genius, but you are very generous too.

Notes:

You may want to edit your post because the two versions differ.
The latter as posted "(day of (cd + 2 * days) < 3)" fails to get
this coming July (where Sat/Sun are the last 2 days). However...

The first version from the same post (using osascript -e) reads:
"((current date) + 3 * days) < 4)" and that does catch 2005 in
its entirety. (YAY!)

I changed my 366 variable to 1500 to push the trial into a four
year period (to catch a leap year). The "+ 3 < 4" values work
splendidly. (Experimentation shows that "+ 3 < 3" also works)!

Anyway, many thanks for this exchange. No matter how small,
it's great to have little snippets of code like this for future use.

Cheers,

-HI-

benhart 02-24-2005 08:17 PM

Perl is your friend!
 
dmacs mentioned Date::Manip earlier, but nobody picked up on it. Date::Manip is the most bloated, powerful, date intuiter ever written. I call it an intuiter instead of a parser because it can nearly tell what date you mean just by intuition! Anyway, here is a one-liner using Date::Manip to solve the problem.

In english, I say 'what is one work day before the first of the month?'

In perl, I say
Code:


my $lastworkday=&DateCalc(&UnixDate('next month', '%b') . " first", "-1 business day");
print &UnixDate($lastworkday, "%c") . "\n";

Or, to see a list of all last workdays this year (well, last december to this november...)
Code:

ben@radix:~$ for i in jan feb mar apr may jun jul aug sept oct nov dec; do  perl -MDate::Manip -e "use Date::Manip; print qq/Date is /, &UnixDate(&DateCalc(qq/$i 1st/,qq/- 1 business days/,\$err), qq/%c/) . qq/.\n/;"; done
Date is Fri Dec 31 08:00:00 2004.
Date is Mon Jan 31 08:00:00 2005.
Date is Mon Feb 28 08:00:00 2005.
Date is Thu Mar 31 08:00:00 2005.
Date is Fri Apr 29 08:00:00 2005.
Date is Tue May 31 08:00:00 2005.
Date is Thu Jun 30 08:00:00 2005.
Date is Fri Jul 29 08:00:00 2005.
Date is Wed Aug 31 08:00:00 2005.
Date is Fri Sep 30 08:00:00 2005.
Date is Mon Oct 31 08:00:00 2005.
Date is Wed Nov 30 08:00:00 2005.

:)

-ben

weltonch777 02-25-2005 02:45 AM

This one isn't as elegant, but it's in bash (portable).

Code:

#/bin/bash
r=8;c=6;while [ = ];do d=$(cal | sed -n $r'p' | awk '{print $'$c'}')
case $d in "")((c--));case $c in 1)((r--));c=6;;esac; ;;
*)case $d in $(date +%d))exit 1;; *)exit 0;;esac; ;;esac;done

Chris

kitzelh 11-28-2008 03:30 PM

A bit more elegant
 
A bit more elegant IMHO:

Code:

#!/bin/bash
[ $(cal|awk '{print $2,$3,$4,$5,$6}'|tr -s '[:blank:]' '\n'|tail -1) = $(date +%d) ]

1) 'cal' outputs the current month's calendar
2) 'awk' strips out the weekends
3) 'tr' changes it from grid-like to a single column of days
4) 'tail' grabs the last day in the list (which is the last weekday of the month)
5) 'date' grabs todays day number
6) the square brackets compare result of #4 and #5
7) The result of the script is the result of the last command run, which in this case is the square brackets.

If you name the above script "islastworkday", then your crontab entry would be:
Code:

15 9 * * * /path/to/script/islastworkday && /usr/local/bin/doSomething

tw 11-28-2008 05:54 PM

the first weekday of every month is trivial in launchd - use the following:
Code:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
        <key>Label</key>
        <string>the.first.weekday</string>
        <key>ProgramArguments</key>
        <array>
                <string>path to script</string>
        </array>
        <key>StartCalendarInterval</key>
        <dict>
                <key>Day</key>
                <integer>1</integer>
                <key>Hour</key>
                <integer>0</integer>
                <key>Minute</key>
                <integer>0</integer>
        </dict>
</dict>
</plist>

if the 1st falls on a weekend when your computer is off, the script will run automatically when you turn the computer back on.

robot_guy 11-28-2008 10:57 PM

Quote:

Originally Posted by bramley (Post 182005)
The following in a shell script will give a '1' if today is the last working day of the current month:

Code:

osascript -e '(((day of ((current date) + 1 * days) = 1) and not ((weekday of (current date) = Saturday) or (weekday of (current date) = Sunday))) or ((weekday of (current date) = Friday) and (day of ((current date) + 3 * days) < 4))) as integer'

Working independently (just for the holiday weekend fun of it) I came up with a slightly different version of the same idea:

Code:

osascript -e "(((day of ((current date) + (1 * days)) = 1) and (weekday of (current date) is in {Monday, Tuesday, Wednesday, Thursday, Friday})) or ((day of ((current date) + (2 * days)) = 1) and (weekday of (current date) is Friday)) or ((day of ((current date) + (3 * days)) = 1) and (weekday of (current date) is Friday))) as integer"
The three different-colored Boolean tests correspond to the three different scenarios for today being the last weekday of the month, as follows:

purple: today is a weekday, and tomorrow is the first day of a new month; or
crimson: today is Friday, and Sunday is the first day of a new month; or
orange-red: today is Friday, and Monday is the first day of a new month.

If today's not a weekday, it's not the last weekday of the month. ;)

tw 11-29-2008 12:52 AM

ok, this is kind of fun... try this (I'm about 80% confident it works):

Code:

osascript -e '(7 - (day of ((current date) + 1 * weeks))) = (((weekday of (current date)) mod 7) - 4) * ((((weekday of (current date)) mod 7) > 3) as integer)'
if it returns 'true' (1) the date should be the last work day of the month

Hal Itosis 11-29-2008 10:25 AM

Bash beauty
 
Quote:

Originally Posted by kitzelh (Post 505614)
  • #!/bin/bash
    [ $(cal |awk '{ print $2, $3, $4, $5, $6 }' |
    tr -s '[:blank:]' '\n' |tail -1) = $(date +%d) ]

Thanks to all who revived this topic... looks like kitzelh's contribution there will be nigh impossible to beat.

Some of the reasons i say that are:
  • Plain vanilla Bash
    While i do appreciate/respect the power of Perl and AppleScript where date "calculations" are concerned,
    it's nice to have a method which does not depend (at all!) on their magic.
  • Uses only *numeric* (string) values
    ...which is pretty cool. Since it uses the 'physical layout' of cal, there is no need to make any reference to
    either "Friday" or "Monday" (or any named day). This means it should work 'as is' for any localization!
    (assuming a call to cal in other countries doesn't have a different layout... like weekends on the right side).
  • No knowledge of future dates needed
    Neat. No subtraction (or any "date-type" math) is involved, looking ahead to Monday to see if it's less than 4.
    Once the output from cal has been sliced and diced, simply check to see if x = y.
  • Only two calls to time-based functions
    One to cal and one to date. No biggie, but -- based on some research i did when this thread began -- the fewer
    such calls inside the script, the less chance something strange might happen (say... if it started 1 second before
    midnight, or near a time zone change... and the CPU is under a heavy load or something. Like i said: no biggie).
  • Fewest chars by far
    About 90 as <kitzelh> posted it (i've added some spaces and a newline, in my red quoted version).
Of course, fans of either Perl or AppleScript can disregard some of those reasons. I will note that having less chars
doesn't mean it runs faster... in fact, it finishes a few milliseconds slower than what i ended up settling on a few years
back... Bash-based with one call to Perl: perl -MPOSIX -le 'print strftime("%e",localtime(time+86400*'$1'))'
to do some date math.

But the fact that kitzelh's Bash-only solution should work on (almost?) any Unix machine
that ever existed... in (almost?) any country in the world, seems pretty spiffy to me. :cool:
Edit: if some old machine doesn't know about character classes, the '[:blank:]' above
can be replaced by a basic quoted space: ' '

Thanks!

Hal Itosis 12-03-2008 12:17 AM

A quick note on size...
By "downgrading" from
  • bash to sh
  • $( ) to ` `
  • [:blank:] to ' '
and, by removing every possible space... kitzelh's (free-standing) cal-based solution
for the "month's last workday" puzzle can be squashed down to a whopping 84 bytes:

#!/bin/sh
[ `cal|awk '{print $2,$3,$4,$5,$6}'|tr -s ' ' '\n'|tail -1` = `date +%d` ]

Whee.

--

Okay.

Out of curiosity, i had to see how this cal-based trick would play out when detecting
the "month's first workday" scenario. As most of the more active participants here
probably noticed: no matter the approach taken --in general-- detecting the month's
*first* workday is somewhat easier than detecting the last.

Right?

Not so with cal. The difference has to do with awk's reliance on cal's physical output.
The success of awking data as $2, $3, etc., depends on a consistent structure whereby
the second field is always Monday, the third field is always Tuesday, and etc., etc., ...
which does hold true from week 2 on. But, the first week of each month is seldom so neat.

Code:

$ cal -m1
    January 2008
Su Mo Tu We Th Fr Sa
      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

There we see (due to two blank days in December) that: $2 is *Wed.* Jan. 2nd.!!!
So we can't simply change 'tail -1' to 'head -1' (ignoring the first two lines for now),
because when awk begins with "$2" it will skip over the actual start: Tues. Jan. 1st.
So those *leading* spaces create an added level of complexity to overcome, which
does not occur when dealing with number sequences at the end of a month.

But another minor issue also surfaced. While `date +%d` works great for double digits,
it prepends a *leading* zero onto single digits: 01, 02, etc. So, that too needed tweaking,
either by changing the [ x = y ] test to [ x -eq y ] -- or by using `date +%e` instead.

Or both.

--

So anyway, I merged kitzelh's solution for "last" with my (derivative) solution for "first"
into one shell script (mostly for experimental purposes... but it's also totally practical).
Its 'output' is purely a matter of exit status (active low for true, per Unix convention).
So it can simply be called by any other script. Else, just copy the few lines which do the
relevant test, and paste them in (either as a function or some 'and list' chain or if/then).

Thanks again for all the fine Perl and AppleScript submissions, but I am hooked on this
(almost "mechanical") cal-based solution. Interesting to note that forbin first suggested
'cal' well over 3 years ago, but i was just getting my feet wet... with much yet to learn.

Code:


#!/bin/bash -
# wdt  :::  (month's first or last) WorkDay Tester
IFS=$' \t\n'
PATH=/bin:/usr/bin
export PATH
PROG=$(basename "$0")

HelpText ()
{
        cat <<-HelpDoc >&2

        Script to ascertain one of two situations:
        ------------------------------------------
          i)  is today this month's  Last workday?
            or
        ii)  is today this month's First workday?
        ------------------------------------------
          +  returns 0 only when condition exists.
          +  does month's Last workday by default.
          +  workdays range from Monday to Friday.
          +  holidays, etc. are not accounted for.

        HelpDoc
        printf 'Usage: \e[1m %s\e[0m ' "$PROG" >&2
        printf '[ \e[1m-l\e[0m | \e[1m-f\e[0m ]\n ' >&2
        printf 'e.g.,\n\tif %s  # no arg (or -' "$PROG" >&2
        printf 'l) tests for "last"\n\tthen\n\t\t# per' >&2
        printf 'form some end-of-month task.\n\tfi\n\n' >&2
}

case $1 in  # parse output fields from *cal* to obtain dates:

  -f*) # Determining the "first workday" requires some extra
        # filtering and processing... performed here with sed

        [ `cal |sed '3,4!d; s/  / @ /g' |
        awk '{ print $2, $3, $4, $5, $6 }' |
        tr -s ' ' '\n' |sed '/@/d; q'` -eq `date +%e` ]

        exit $?  # 0 means first, 1 means not first
        ;;

-[^l]*) HelpText
        ;;

    *) # The "last workday" solution below originally provided by kitzelh
        # http://forums.macosxhints.com/showpost.php?p=505614&postcount=17

        [ `cal |awk '{ print $2, $3, $4, $5, $6 }' |
        tr -s ' ' '\n' |tail -1` -eq `date +%e` ]

        exit $?  # 0 means last, 1 means not last
        ;;
esac
exit 2  # error (incorrect arg or help request)

--

Just want to add some thoughts about 'application'. I did say in post #1 that the computer
would be off over the weekend. But that over-simplifies matters too much. Maybe the Mac
is left on but some peripheral... or network connection... or special person is gone for the
weekend. If the event fires too soon (such as with the launchd Day 1 offering), it could be
an undesirable situation. This was really more about precision in when an event triggers...
not about making up some missed task, *or* launching prematurely -- and relying on the
hope someone didn't forget to turn off the Mac. (who knows, this might be a portable that
is on all weekend... but simply isn't connected to the office network and/or peripherals at
that time). Too many unknowns to cover every possibility, so it's best to have things start
off at clearly defined times. [be it either first or last "workday".]

-HI-

tw 12-03-2008 08:29 PM

Quote:

Originally Posted by Hal Itosis (Post 506326)
...

man, I wish I had that much free time on my hands... :D

Hal Itosis 12-03-2008 10:49 PM

Quote:

Originally Posted by tw (Post 506520)
man, I wish I had that much free time on my hands... :D

oh idunno...
tw
MVP
Join Date: Apr 2007
Posts: 1,965

Hal Itosis
MVP
Join Date: Apr 2002
Posts: 1,519

Your total post count (over time),
looks quite healthy... next to mine.

;)

tw 12-03-2008 11:04 PM

Quote:

Originally Posted by Hal Itosis (Post 506538)
oh idunno...
tw
MVP
Join Date: Apr 2007
Posts: 1,965

Hal Itosis
MVP
Join Date: Apr 2002
Posts: 1,519

Your total post count (over time),
looks quite healthy... next to mine.

;)

lol - details, details... :p

Michael_S 02-28-2010 12:25 PM

Compact
 
Quote:

Originally Posted by Hal Itosis (Post 506326)
... kitzelh's (free-standing) cal-based solution
for the "month's last workday" puzzle can be squashed down to a whopping 84 bytes:

#!/bin/sh
[ `cal|awk '{print $2,$3,$4,$5,$6}'|tr -s ' ' '\n'|tail -1` = `date +%d` ]

Ahh, you want compact?

Code:

#!/bin/sh
[ `cal|awk '$2{w=$7?$6:$NF}END{print w}'` = `date +%d` ]

66 bytes, without the newline, 20% less bytes, and 2 processess down ;) It's one more down in bash's since [ .. ] (or [[ .. ]]) are builtins.

Brief explanation:
  • Look only at lines that have $2 present.
  • The last such item in a month is a week that has a Monday.
  • If that week has a Saturday ($7), grab the Friday before ($6); otherwise, grab the last day ($NF).
  • After all is read, print the last day, and handle it as kitzelh.
BTW, the comparison against date +%d works only for two-digit days of the month, since it applies zero-padding. For the context of first days of the month, better use %e (space-padded). Unless you double-quote the backtick-expression, bash will then ignore the space.

Late reply once more ... Just came across your thread and ideas while Googling the question.

Hal Itosis 03-02-2010 02:14 AM

Quote:

Originally Posted by Michael_S (Post 574186)
Ahh, you want compact?
Code:

#!/bin/sh
[ `cal|awk '$2{w=$7?$6:$NF}END{print w}'` = `date +%d` ]


Incredible.

Quote:

Originally Posted by Michael_S (Post 574186)
66 bytes, without the newline, 20% less bytes, and 2 processess down ;)

Yes... thanks for pointing that out. :mad: (( :) ))


Quote:

Originally Posted by Michael_S (Post 574186)
Brief explanation:
  • Look only at lines that have $2 present.
  • The last such item in a month is a week that has a Monday.
  • If that week has a Saturday ($7), grab the Friday before ($6); otherwise, grab the last day ($NF).

Ingenious.

Thanks for chiming in... it was well worth seeing such elegance, even at the cost of mild embarassment (it sure seemed like this case was closed). Oh well... at least my previous solution was bug-free, if not the epitome of efficiency.

:D

Quote:

Originally Posted by Michael_S (Post 574186)
BTW, the comparison against date +%d works only for two-digit days of the month, since it applies zero-padding. For the context of first days of the month, better use %e (space-padded). Unless you double-quote the backtick-expression, bash will then ignore the space.

Agreed. (Indeed, the "+%e" form was favored in my post #22).

§

So now... in applying those same principles to detecting the corresponding 'first workday' condition (with its slightly differing physical challenges), I came up with this:

[ `cal|awk 'NR==3{w=$7?2:1;w=$2?w:3;print w}'` = `date +%e` ]

How'd i do? Can that be shortened? Methinks not.

Here it is all stretched out, for easier reading:
[ `cal |awk 'NR==3 { w = $7 ? 2 : 1; w = $2 ? w : 3; print w }'` = `date +%e` ]

Brief explanation:
  • Look only at line 3, and deal with 3 possible outcomes: the month's first workday date will be either 1, 2 or 3.
  • If field 7 exists, the 1st fell on Sunday... so Monday the 2nd is that month's first worday. Else, set w to 1 (as odds favor that possibility).
  • But also test for only one field (or equivalently: the lack of $2), since that would mean Saturday was the 1st, and therfore Monday the 3rd is that month's first workday.
Whew.

Thanks again Michael. Good stuff.

-HI-

Michael_S 03-03-2010 12:17 AM

Hi Hal,

Glad you like it, no embarrasment intended. Sorry, I missed your mention of %e at the first reading.

Quote:

Originally Posted by Hal Itosis (Post 574354)
So now... in applying those same principles to detecting the corresponding 'first workday' condition (with its slightly differing physical challenges), I came up with this:

[ `cal|awk 'NR==3{w=$7?2:1;w=$2?w:3;print w}'` = `date +%e` ]

How'd i do? Can that be shortened? Methinks not.

Good analysis and reduction to output constants. Applying your logic, though, can be shortened by chaining the two ternary ?: operators into one expression:
Code:

[ `cal|awk 'NR==3{print $7?2:$2?1:3}'` = `date +%e` ]
;)

The awk expression can be written more readably following the style suggested by Damian Conway in Perl Best Practices, Chapter 2: "Ternaries":
Code:

NR == 3 {
    print $7 ? 2 \
        : $2 ? 1 \
        :      3
}


(kendall) 02-13-2011 05:49 AM

Anyone want to see another way?
 
I realize that the original post was back in the early days of Tiger, so this version may be considered a bit of a cheat. Since Leopard (late 2007) the date command has had a feature that the GNU version of date has had as long as I can remember: the ability to do date math. Read about the -v option in the date man page for all the details. Here it is:

Code:

date -v+3d +%d%w | egrep -q "03[^23]|0[12]1"
44 characters. Short enough to stick in a crontab directly:

0 9 26-31 * * date -v+3d +\%d\%w | egrep -q "03[^23]|0[12]1" && /usr/local/bin/doSomething

This will do something at 9AM on the last weekday of each month. Alas, 2 backslashes are required to keep the format string intact. That makes 46 characters, but you can get them back by removing the optional spaces around the pipe.

How it works:
The -v+3d means we are looking at the date 3 days from now. The format string +%d%w has the date command outputting a 3 digit number. The first two digits are the day of the month the 3rd digit is the day of the week (sun=0, mon=1, but you knew that). There are many possible outputs, but the only ones that interest us are the ones where the date is 03 and the day of the week is not tuesday or wednesday (because that means you ran the command on saturday or sunday), and where the date is 01 or 02 and the day of the week is monday (because you ran it on friday). In other words:
030 031 034 035 036 011 021. The egrep command looks for these patterns.

Portability note: For those who may have GNU date, replace -v+3d with -d3day.

(kendall) 02-14-2011 04:41 PM

First day of the month
 
Here's a first-weekday-of-the-month crontab too.

0 9 1-3 * * date +\%d\%w | egrep -q "01[1-5]|0[23]1" && /usr/local/bin/doSomething

40 characters. It uses similar logic to the previous post, but doesn't require date math. This will work with any version of the date command.

(kendall) 02-14-2011 06:37 PM

Another Last weekday of the month
 
cal|cut -c4-20|tr -d '\n'|grep -q `date +\%d`$

46 characters. Works with any date command.

I need to get a life.

Hal Itosis 02-28-2011 01:00 AM

Quote:

Originally Posted by (kendall) (Post 611417)

cal|cut -c4-20|tr -d '\n'|grep -q `date +\%d`$


46 characters. Works with any date command.

Spaktakular. The best ever.

i found a minor bug though: cut -c4-20 has too many columns (because it includes Saturdays). So it will be wrong on months which end on Saturday (e.g., April and December this year).
Code:

$ cal -m4 |cut -c4-20
  April 2011
Mo Tu We Th Fr Sa
            1  2
 4  5  6  7  8  9
11 12 13 14 15 16
18 19 20 21 22 23
25 26 27 28 29 30

The 29th will be April's last workday (but grep -q `date +%d`$ will never match that, due to the anchor at the end). Anyway, i guess you probably intended to clip off the weekends... and simply forgot to do it on both sides. Therefore, the magic number is 17.
Code:

$ cal -m4 |cut -c4-17
  April 2011
Mo Tu We Th Fr
            1
 4  5  6  7  8
11 12 13 14 15
18 19 20 21 22
25 26 27 28 29

Thus the full command becomes:
cal|cut -c4-17|tr -d '\n'|grep -q `date +%d`$
[45 chars total, since i also dumped the backslash]

Since today (Monday, February 28th) is the last workday this month... we should get a zero for the exit status.
Code:

$ cal|cut -c4-17|tr -d '\n'|grep -q `date +%d`$
$ echo $?
0

Nice.

--

P.S. the others you cooked up using the blended 'day-week' numbers (date +%d%w), and then egrepping for those unique patterns was also quite ingenious:
date -v+3d +%d%w |egrep -q '03[^23]|0[12]1'
Pretty slick.

Thanks Kendall.

fracai 02-28-2011 01:20 PM

I suppose this doesn't matter given how it's doing a grep at the end of the string, but "cut -c4-17" ends up running some of the dates together (the last on a line with the first on the next line).


Code:

February 2011
Mo Tu We Th Fr
    1  2  3  4
 7  8  9 10 11
14 15 16 17 18
21 22 23 24 25
28

turns into:
Code:

February 2011Mo Tu We Th Fr    1  2  3  4 7  8  9 10 1114 15 16 17 1821 22 23 24 2528
This can be fixed (again, not that it even matters) by using "cut -c4-18".
Code:

February 2011Mo Tu We Th Fr    1  2  3  4  7  8  9 10 11 14 15 16 17 18 21 22 23 24 25 28
Though, this will still have problems with the Month and Year and the first date.

Or, use "xargs" instead of the "tr -d '\n'". It's shorter anyway :-) And, we're back to 40 characters.

Code:

cal|cut -c4-17|xargs|grep -q `date +%d`$

Hal Itosis 02-28-2011 02:32 PM

Quote:

Originally Posted by (kendall) (Post 611245)
I realize that the original post was back in the early days of Tiger, so this version may be considered a bit of a cheat. Since Leopard (late 2007) the date command has had a feature that the GNU version of date has had as long as I can remember: the ability to do date math. Read about the -v option in the date man page for all the details. Here it is:

Code:

date -v+3d +%d%w | egrep -q "03[^23]|0[12]1"
44 characters. Short enough to stick in a crontab directly:

0 9 26-31 * * date -v+3d +\%d\%w | egrep -q "03[^23]|0[12]1" && /usr/local/bin/doSomething

This will do something at 9AM on the last weekday of each month. Alas, 2 backslashes are required to keep the format string intact. That makes 46 characters, but you can get them back by removing the optional spaces around the pipe.

How it works:
The -v+3d means we are looking at the date 3 days from now. The format string +%d%w has the date command outputting a 3 digit number. The first two digits are the day of the month the 3rd digit is the day of the week (sun=0, mon=1, but you knew that). There are many possible outputs, but the only ones that interest us are the ones where the date is 03 and the day of the week is not tuesday or wednesday (because that means you ran the command on saturday or sunday), and where the date is 01 or 02 and the day of the week is monday (because you ran it on friday). In other words:
030 031 034 035 036 011 021. The egrep command looks for these patterns.

Portability note: For those who may have GNU date, replace -v+3d with -d3day.

Working off of that clever technique, i will propose this as a possible minimal (37 char) solution:

date -v+1d '+%d%w' |grep -q '01[^01]'

i need to test it some more, and will explain further if successful.

[quick edit] -- note even that could be boiled down to 32-bytes, by removing quotes and spaces:
date -v+1d +%d%w|grep -q 01[^01]

Doesn't cover enough ground. Something still seems strange about kendall's version tho... i need to see if it's in the explanation or in the code (or just me).


EDIT:

Nope... kendall's line in that one is right on. It just looks strange because our calendar arrangement is kinda strange (with its varying number of days in a month, and so forth).

So aside from not working in Tiger, this method of his is a better way to go (IMO):
date -v+3d '+%d%w' |egrep -q '03[^23]|0[12]1'
The reasons i say that are similar to those i gave in post #21 above. Mainly because it uses only a single call to a command that has to request time-based info.

For example, compared to this method:
cal|cut -c4-17|tr -d '\n'|grep -q `date +%d`$

That procedure contains five command calls —including the substitution —connected via three pipes (whereas the former has only two command calls joined by one pipe). Of the five commands it employs, two (cal and date) are gathering time-based information. Although incredibly slim, there is a slight chance (say if the script were run 10 milliseconds before midnight perhaps) that cal might read its info before midnight, and then date might read its info a few milliseconds into the "next" day. Though not very likely to happen, it would produce an error.

fracai 02-28-2011 09:13 PM

Valid point on the time issue.

And I just spent around a half hour trying to figure out why your "+1d" variation didn't work. Finally when I was printing out each day sequentially I realized that it's because +1 doesn't search far enough past the weekend. It's funny how something so obvious just sails right by sometimes.

As for the shortest method, my wife had the comment, "Isn't the shortest method to just pick the first one you get and go with it? Instead of you computer nerds debating it for 3 hours. It can't be more than 3 lines of code." There is some wisdom there.

I just showed her that the first post was in 2005. Now she's just starring at me and repeating "Six years. SIX YEARS!" "At this point I could have just pointed to a calendar and listed all of them."

Hal Itosis 02-28-2011 10:14 PM

Quote:

Originally Posted by fracai (Post 613498)
As for the shortest method, my wife had the comment, "Isn't the shortest method to just pick the first one you get and go with it? Instead of you computer nerds debating it for 3 hours. It can't be more than 3 lines of code." There is some wisdom there.

I just showed her that the first post was in 2005. Now she's just starring at me and repeating "Six years. SIX YEARS!" "At this point I could have just pointed to a calendar and listed all of them."

:D No way to explain it i guess. This thread is probably the most in-depth across the web on the topic (strictly from a Bash perspective... since Perl has libraries loaded with stuff like this). Tomorrow, and into the future, folks will be wanting to do this... so they'll google and wind up here, where they can instantly benefit from all our research. [indeed, some of the solutions herein were obtained from scripters who just dropped by, having googled their way here... and who knows how many have simply copied the code and cut out, without registering to comment.]

And now i've updated my "wdt" shell script to incorporate kendall's latest date -v |grep -E coolness.

But yeah, spouses (including my gf) just don't 'get' it. :)

(kendall) 03-02-2011 02:10 PM

Quote:

Originally Posted by Hal Itosis (Post 613364)
Spaktakular. The best ever.

i found a minor bug though: cut -c4-20 has too many columns (because it includes Saturdays). So it will be wrong on months which end on Saturday (e.g., April and December this year).
Code:

$ cal -m4 |cut -c4-20
  April 2011
Mo Tu We Th Fr Sa
            1  2
 4  5  6  7  8  9
11 12 13 14 15 16
18 19 20 21 22 23
25 26 27 28 29 30

The 29th will be April's last workday (but grep -q `date +%d`$ will never match that, due to the anchor at the end). Anyway, i guess you probably intended to clip off the weekends... and simply forgot to do it on both sides. Therefore, the magic number is 17.
Code:

$ cal -m4 |cut -c4-17
  April 2011
Mo Tu We Th Fr
            1
 4  5  6  7  8
11 12 13 14 15
18 19 20 21 22
25 26 27 28 29

Thus the full command becomes:
cal|cut -c4-17|tr -d '\n'|grep -q `date +%d`$
[45 chars total, since i also dumped the backslash]

Since today (Monday, February 28th) is the last workday this month... we should get a zero for the exit status.
Code:

$ cal|cut -c4-17|tr -d '\n'|grep -q `date +%d`$
$ echo $?
0

Nice.

--

P.S. the others you cooked up using the blended 'day-week' numbers (date +%d%w), and then egrepping for those unique patterns was also quite ingenious:
date -v+3d +%d%w |egrep -q '03[^23]|0[12]1'
Pretty slick.

Thanks Kendall.

Doh! Thanks for catching that. That post was an afterthought, and I did not obsess over it nearly as much as post #29.

(kendall) 03-02-2011 02:14 PM

Quote:

Originally Posted by fracai (Post 613454)
I suppose this doesn't matter given how it's doing a grep at the end of the string, but "cut -c4-17" ends up running some of the dates together (the last on a line with the first on the next line).


Code:

February 2011
Mo Tu We Th Fr
    1  2  3  4
 7  8  9 10 11
14 15 16 17 18
21 22 23 24 25
28

turns into:
Code:

February 2011Mo Tu We Th Fr    1  2  3  4 7  8  9 10 1114 15 16 17 1821 22 23 24 2528
This can be fixed (again, not that it even matters) by using "cut -c4-18".
Code:

February 2011Mo Tu We Th Fr    1  2  3  4  7  8  9 10 11 14 15 16 17 18 21 22 23 24 25 28
Though, this will still have problems with the Month and Year and the first date.

Or, use "xargs" instead of the "tr -d '\n'". It's shorter anyway :-) And, we're back to 40 characters.

Code:

cal|cut -c4-17|xargs|grep -q `date +%d`$

Even though I used the wrong cut command arguments, I was aware that the dates were being run together, and wondered if anyone would notice. You're correct that it doesn't matter (since grep is only looking at the end of the line). I left that ugliness as sort of an easter egg for those who might actually run my code. :)

But thanks for the xargs tweak. I love it!

Hal Itosis 03-08-2011 02:56 AM

The man page for cal in Leopard talks a good game about ncal but (unlike 10.6.x), it doesn't seem that 10.5 can actually execute any ncal functions. At least that appears to be the case on my sole Leo machine.

Quick question: can someone with 10.5.x confirm (or refute) that that is indeed the case? [i.e., man page for cal claims that ncal exists... but Leopard in fact fails to run it.]

hayne 03-08-2011 11:48 AM

Quote:

Originally Posted by Hal Itosis (Post 614470)
can someone with 10.5.x confirm (or refute) that that is indeed the case? [i.e., man page for cal claims that ncal exists... but Leopard in fact fails to run it.]

See: http://www.macworld.com/article/1335...lterminal.html

Hal Itosis 03-08-2011 01:14 PM

Thanks hayne.

(sheesh, Apple couldn't have added a simple symlink to any of the 10.5.x updates?!?! :rolleyes:)


All times are GMT -5. The time now is 10:21 PM.

Powered by vBulletin® Version 3.8.7
Copyright ©2000 - 2014, vBulletin Solutions, Inc.
Site design © IDG Consumer & SMB; individuals retain copyright of their postings
but consent to the possible use of their material in other areas of IDG Consumer & SMB.