![]() |
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 commandLimiting it to weekdays only is also simple: Code:
#min hour mday month wday command-- 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] |
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. |
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 |
Almost got it.
February is the tricky part. This version runs twice in Feb (sometimes): Code:
#min hour mday month wday commandYeah, 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: |
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 |
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...
|
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? ;) |
Quote:
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) |
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 2005That'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:
|
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'[EDIT - Whoops! Doesn't work if the last day of the month is on a weekend] |
Quote:
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 {} |
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 ¬ |
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'Code:
on testForMonthsLastWorkday(cd) ------------------------------------------- |
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- |
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:
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-ben |
This one isn't as elegant, but it's in bash (portable).
Code:
#/bin/bash |
A bit more elegant
A bit more elegant IMHO:
Code:
#!/bin/bash2) '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 |
the first weekday of every month is trivial in launchd - use the following:
Code:
<?xml version="1.0" encoding="UTF-8"?> |
Quote:
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"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. ;) |
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)' |
Bash beauty
Quote:
Some of the reasons i say that are:
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! |
A quick note on size...
By "downgrading" from
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 -m1So 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:
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- |
Quote:
|
Quote:
tw Your total post count (over time), looks quite healthy... next to mine. ;) |
Quote:
|
Compact
Quote:
Code:
#!/bin/shBrief explanation:
Late reply once more ... Just came across your thread and ideas while Googling the question. |
Quote:
Quote:
Quote:
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:
§ 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:
Thanks again Michael. Good stuff. -HI- |
Hi Hal,
Glad you like it, no embarrasment intended. Sorry, I missed your mention of %e at the first reading. Quote:
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 { |
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"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. |
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. |
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. |
Quote:
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-20Code:
$ cal -m4 |cut -c4-17cal|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`$-- 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. |
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 2011Code:
February 2011Mo Tu We Th Fr 1 2 3 4 7 8 9 10 1114 15 16 17 1821 22 23 24 2528Code:
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 28Or, 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`$ |
Quote:
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. |
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." |
Quote:
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. :) |
Quote:
|
Quote:
But thanks for the xargs tweak. I love it! |
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.] |
Quote:
|
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.