Function takes a very lot of CPU load for long time

OK, I think I’ve ironed out the bugs in my earlier code (test file below). The function doing the work:

function fNumberWorkDays(iStart:date,iEnd:date){

var:number vDays = days(iStart,iEnd);

// day of week of start day
var:number vStartDay = iStart.weekday;

// set a result var for number of workdays in start/end duration
var:number vWorkDays=0;

// number of weekdays within whole weeks 
var:number vWorkWeekDays = floor(vDays/7)*5;

// number of days in remaining partial week
var:number vPartialWeekDays = mod(vDays,7);

// work out number of weekdays duration
//  add item variables
var:number vNumber1=0;
var:number vNumber2=0;

if(vWorkWeekDays==0){
  vNumber1 = vStartDay+vPartialWeekDays;
  if(vNumber1>7&vStartDay<6){vNumber2 = vNumber1-vStartDay-2;};
  if(vNumber1==7){vNumber2 = vNumber1-2;};
  if(vNumber1==6){vNumber2 = vNumber1-1;};
  vWorkDays = vNumber2;
}else{
  if(vPartialWeekDays>0){
    // i.e. 1-6 days of a part week
    vNumber1 = vStartDay+vPartialWeekDays;
    // if interim count 
    if(vNumber1>7&vStartDay<6){vNumber2 = vNumber1-vStartDay-2;};
    if(vNumber1==7){vNumber2 = vNumber1-2;};
    if(vNumber1==6){vNumber2 = vNumber1-1;};
    vWorkDays = vWorkWeekDays+vNumber2;
  }else{
    vWorkDays = vWorkWeekDays;
  };
};

return vWorkDays;
}; ///END

In the test file, in the library note for the above function there is a copy of the function called fDebugNumberWorkDays. It does the same as the above but logs some of the interim variables into a note at path /log.

The function is called for note at path /Text via this edict:

$MyNumber = fNumberWorkDays($StartDate,$EndDate);

Buy in actual use I’d put this as an $Edict of a prototype (and setting the prototype’s $EdictDisabled to true).

Here is the file: ElaspedWorkDays.tbx (173.2 KB)

WARNING. This code cannot account for:

  • national/public holidays
  • winter/summer time shift using the tested period
  • leap years
  • … any other weird calendar edge cases!

Command line. A quick web search suggests my hunch was wrong about a ‘simple’ command line solution. However, some built-in code languages like Python appear to be able to do this sort of calculation.

I’d be interested to know how this works for you (and any bugs if found, outside the known warnings above).

1 Like

Thank you all for all very much for all the effort that you put into my problem. I much appreciate that !
I’ve tested all your suggestions and compared it with a manual count from the calendar:

April 2024
mo tue wed thu fri sat sun mo tue wed thu fri sat sun mo tue wed thu fri sat sun mo tue wed thu fri sat sun
 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

@PaulWalters
Your code is simple. I like that but alas… the results are not what I was hoping for:

$MyNumber=(interval($DatumIn,$DatumUit)/7)*5;

22-4-2024 - 26-04-2024 = 2 (Should be 4, (1st day counts)
18-04-2024 - 26-04-2024 = 5 (Should be 6)
01-04-2024 - 26-04-2024 = 17 (should be 19)

I think the cause of this is that every period of 7 days is recalculated to 5. When there are no or more weekend(s) involved the calculation fails.

@eastgate
Very nice and simple code and formatting the date was indeed unnecessary. unfortunately I didn’t get the code to work.

function fDoorloopTijd2() {

var:date vStart = $DatumIn; //format("D-M0-y");
var:date vEnd = $DatumUit; //.format("D-M0-y");
var:date vLoopDate = vStart; //.format("D-M0-y");
var:num vDagen = days(vStart,vEnd);
var:list vSetDagen=;
var:num vDoorloop=;
var:interval vDay=interval("24:00");

vLoopDate = vLoopDate + vDay; //Dag van ontvangst telt niet mee

	while(vLoopDate<vEnd){
		if(vLoopDate.weekday<6) {vDoorloop+=1;};
		$Doorlooptijd= vDoorloop;
	};
};

I changed the last line {$Doorlooptijd+=1} and hope that I interpreted the rest of your code correctly.
But this code gives constant different outcomes. I will take some time this weekend to figure out what is happening. :slight_smile:

@mwra
You made a beautiful piece of code. I very much like the way you have set it up!
I will need some time to comprehend what exactly is happening. But also this code doesn’t give me the outcome I am looking for .

22-4-2024 - 26-04-2024 = 0 (Should be 4)
18-04-2024 - 26-04-2024 = 5 (Should be 6)
01-04-2024 - 26-04-2024 = 15 (should be 19)

For now I think I will make the calculation in Excel as @PaulWalters suggested and take some time to figure out what the right approach is.
Of course I would love to hear if anyone has a nice solution, but for now thanks again for your efforts.

Because I provided a simple example, with caveats, to demonstrate the math. It is pseudo-code, not production code. @mwra expanded on the idea, and gave you production-ready code that addresses your major issues. However, as we both noted, the function does not take into account holidays or other non-working periods that we don’t know about.

Ups, my bad. I misinterpreted :see_no_evil:

I noticed that one and I don’t mind. I just do not want to count the Saturday’s and Sunday’s

Thanks for your clarification.

Oh dear, that is the case. My mistake!

So, I started over with different logic. See this re-coded file: ElaspedWorkDays2.tbx (193.1 KB)

The approach is braodly this:

  • we work out the next weekday-number after the start date whose weekday number matches that of the end date. We make a new date for the ‘offset’ start. That allows us to deal with complete weeks and get a 5 day/week day count back. If the start/end are already the same weekday number job is complete.
  • otherwise we work of the number of workdays in the ‘remainder’ from the real start date to the offset start date. Add that to the whole-week derived count and we are done.

…see the code in the “Work Days’” library note.

I’ve used more functions here and tried to add meaningful comments. Note the use of a list to general day-scoped intervals for 1 through 7 day intervals, noting @eastgate’s point about as adding an interval to a date being faster than adding a ‘N day’ string.

The above errors are now fixed and I hope there are no new ones.

Save caveats apply re holidays, summer time, etc.

I hope this helps :slight_smile:

Thank you for this new set of code. The results are exactly what I had in mind. It taught me a few new things such as using a function within a function.

Don’t get me wrong but isn’t it a lot of code for a (in my mind) relative simple calculation?
Maybe I got it all wrong but I experimented with my old code and some of the ideas I took from the various tips here and came to this:

function fDoorloopTijd() {

var:date vStart = $DatumIn; //theStartdate
var:date vEnd = $DatumUit; //the Enddate
var:date vLoopDate = $DatumIn; //set the start of the loop
var:num vDagen = days(vStart,vEnd); //Count the total number of days between start and enddate
var:list vSetDagen =; //variable to hold the weekdays from start to end
var:num vTeller =; //counter used for the loop
var:interval vDay = interval("24:00:00"); //used to increment the loopdate

// The routine lists the days of the week from start to finish
	while(vTeller<vDagen){
		vSetDagen+=vLoopDate.weekday;
		vTeller+=1;
		vLoopDate+=vDay;
		$Doorlooptijd = vSetDagen.count_if(anItem,anItem<6);  //do not count Weekenddays 
	};
};

I changed the vDay interval to interval(“24:00:00”), I noticed an near endless loop when I used “24:00”. Probably because with “24:00” it counted the minutes (?)

So it seems that both functions produce the right outcome and -most importantly- Tinderbox doesn’t stall by the edict when you open the document, nor does it stall during the periodic updates from the edicts.

Learned a lot :slight_smile:

A lot of code? I’ve stripped out some duplication and it now looks like:

function fNumberWorkDays(iStart:date,iEnd:date){
  var:number vStartDay = iStart.weekday;
  var:number vEndDay = iEnd.weekday;
  var:number vStartDaysOffset =;
  var:date vOffsetStartDate =;
  var:number vWorkWeekDays = 0;
  if(vStartDay==vEndDay){
    vWorkWeekDays = fGetWorkDaysOnly(iStart,iEnd);
    return vWorkWeekDays;
  };
  var:number vRemainderWorkDays = 0;
  if(vStartDay<vEndDay){vStartDaysOffset = vEndDay - vStartDay;};
  if(vStartDay>EndDay){vStartDaysOffset = (vEndDay +7) - vStartDay;};
  vOffsetStartDate = iStart + fDaysAsInteval(vStartDaysOffset);
  vWorkWeekDays = fGetWorkDaysOnly(vOffsetStartDate,iEnd);
   vRemainderWorkDays = fGetRemainderWorkDays(vStartDay,vEndDay);
  vWorkWeekDays += vRemainderWorkDays;
  return vWorkWeekDays;
};////END

function fGetWorkDaysOnly(iStart:date, iEnd:date){
  var:number vDays = days(iStart,iEnd);
  vDays = floor(vDays/7)*5;
  return vDays;
} ////END

function fDaysAsInterval(iDays:number){
  var:list vDayList =[1 day;2 days;3 days;4 days;5 days;6 days;7 days];
  var:interval vInt = (vDayList.at(iDays-1));
  return vInt;
} ////END

function fGetRemainderWorkDays(iStartDay:number,iEndDay:number){
  if(iStartDay>5){iStartDay = 1;};
  if(iEndDay>5){iEndDay = 5;};
  var:number vOffset = 0;
  if(iStartDay<iEndDay){vOffset = iEndDay - iStartDay;};
  if(iStartDay>iEndDay){vOffset = (iEndDay + 5) - iStartDay;};
  return vOffset;
} ////END

Indeed, some of these could be further compressed, e.g. to lose a function:

vWorkWeekDays = fGetWorkDaysOnly(vOffsetStartDate,iEnd);

becoming:

vWorkWeekDays = floor(days(vOffsetStartDate,iEnd)/7)*5;

but is it easier to understand? I’m sure I could make the above shorter (and less easily understood) if code length were a stylistic criteria. We can lose another functions by replacing :

vOffsetStartDate = iStart + fDaysAsInteval(vStartDaysOffset);

with

vOffsetStartDate = iStart + interval(vStartDaysOffset+" days");

I’m not an expert coder but fewer lines does not always mean more efficient plus, code in conditional branch is only run if the opening condition is met.

A point to consider is scale. For instance, the code using a while() loop takes longer as the duration of the task gets longer as each day within the task has to be evaluated in turn (more days, more loops run). Then again, generally, if the document is not slow running, then good enough is good enough as once the code runs correctly/to satisfaction there is little value if re-editing it except for learning purposes.

True … it’s all from a certain point of view. :smiley:

Good point! To be honest I didn’t think of that. Usually the dates are within a range of 2-14 days so the impact won’t be great. But still…

So squeezing out expanded bits of code and comment we get:

function fNumberWorkDays(iStart:date,iEnd:date){
  var:number vStartDay = iStart.weekday;
  var:number vEndDay = iEnd.weekday;
  var:number vStartDaysOffset =;
  var:date vOffsetStartDate =;
  var:number vWorkWeekDays = 0;
  if(vStartDay==vEndDay){ return fGetWorkDaysOnly(iStart,iEnd);};
  var:number vRemainderWorkDays = 0;
  if(vStartDay<vEndDay){vStartDaysOffset = vEndDay - vStartDay;};
  if(vStartDay>EndDay){vStartDaysOffset = (vEndDay +7) - vStartDay;};
  vOffsetStartDate = iStart + interval(vStartDaysOffset+" days");
  vWorkWeekDays = floor(days(vOffsetStartDate,iEnd)/7)*5;
  vRemainderWorkDays = fGetRemainderWorkDays(vStartDay,vEndDay,0);
  return (vWorkWeekDays + vRemainderWorkDays);
};

function fGetRemainderWorkDays(iStartDay:number,iEndDay:number,iOffset:number){
  if(iStartDay>5){iStartDay = 1;};
  if(iEndDay>5){iEndDay = 5;};
  if(iStartDay<iEndDay){iOffset = iEndDay - iStartDay;};
  if(iStartDay>iEndDay){iOffset = (iEndDay + 5) - iStartDay;};
  return iOffset;
};

Of which 2 lines are also essentially conditional branches (i.e. the same code cannot match both queries, even though the code is expressed as 2x if() and not an if()...else

So 22 lines to your 15, about a third more. :slight_smile:

It does occur to me that leap years aren’t a problem as they simply sit in the calendar without distorting the weekday numbering. Summer time is also not a problem as we aren’t checking times. So what about public holidays? … see next post.

1 Like

:ok_hand: :+1: :smiley:

Public Holidays… That would be a nice extension!

For holidays, there are probably c.6–10 (varies by country) a year and we only care about those falling on weekdays, and only those falling within the earliest/latest date of our project. We could store them like so (I’m making up the dates and assuming they are weekdays):

var:list vHolidays = [2024-04-01;2024-05-01;2024-05-31];

Now in your loop, you insert:

if(vHoliday.count_if(aDay,aDay==(vLoopDate.format("==")))==1){$Doorlooptijd-=1;};

As you loop you check the yyy-mm-dd string of vLoopDate. If the latter matches a holiday a count of 1 arises, and the if() is matched and we de-increment $Doorlooptijd by 1.

For my solution we’d have to use a list of Date object and then do something like:

var:number vCount=0;
vHolidays.each(aDay){
  if(aDay>=iStart & aDay<=iEnd){vCount-=1;};
};
// then use vCount to decrement out overall workday tally.

This is all a bit baroque, but possible, I think. Note though: I have not tested either of these two holiday test code samples. Also, I don’t think there is an automated way to set up the list of holiday dates.

As all this code is running in a function, you could add an extra boolean argument to the outer functions allowing you to decide whether to do the extra overhead of holiday checking or not by placing the holiday code inside an if() test.

†. Not being tested, I’m not sure if you can use Date.format() inside a query.

Terrific! I’m going to continue experimenting with that.

I continued testing Your and My code with dates that crosses a year. It looks like something goes wrong in both.

To be continued…

1 Like

FWIW, here is my solution reduced down to just 2 functions, in case anyone wants to use that solution:
ElaspedWorkDays3.tbx (186.2 KB) error in code (reported down-thread) Fixed: ElaspedWorkDays3.tbx (186.4 KB)

N.B. not tested for the crossing-year-boundary just reported above
Testing, the count seems correct across a year end. 21 Dec 2023 - 03 Jan 2024 gives 9 (ignoring actual public holidays). The year end tends to have a good number of public holidays so an option for that might be good!

So, this works:

var:list vHolidays = [2023-12-25;2023-12-26;2024-01-01;2024-03-29];

$StartDate = date("21 Dec 2023");
$EndDate = date("3 Jan 2024");

var:number vCount=0;
vHolidays.each(aDay){
  if(date(aDay)>date($StartDate-"1 day") & date(aDay)<date($EndDate+"1 day")){vCount-=1;};
};
$Text = vCount;

as $Text is now “-3” and there are indeed 3 (UK) public holidays (as defined overall in vHolidays) between the test’s start and end dates.

Why am I subtracting a day from the start and adding 1 at the end? Whilst == and != ignore the time element of the date, the greater-/less-than/or equal to operators (< > <= >=) do allow for time. So, to ensure a match to holidays on the start/end dates, we set one day outside to get a true count.

Yes - I’ve not forgotten the fact revealed by @eastgate (up thread—who knew!) that adding an Interval of 1 day is faster/more-efficient than via a “+1 day” string, in date() but that would be more code :slight_smile: But, that approach could be done.


Separately, in this

I don’t think the $Doorlooptijd =... line needs to be in the loop. Why? the first line of the while() loop increments vSetDagen, so we only need to read it and count the weekdays into $Doorlooptijd once the while is done.

However, that means my holiday test for you code would need to store the holiday count in aggregate for the while loop and then, outside the loop, decrement $Doorlooptijd.

An interesting end-fof-week puzzle!

Have we understood at this point why the original function was taking so long to complete? Or is that still a mystery?

No, but a contributory factor is likely this:

Every loop involved two date to string conversions. By comparison the later code posted at msg #

var:num vDagen = days(vStart,vEnd); //Count the total number of days between start and enddate
...
var:num vTeller =; //counter used for the loop
...
	while(vTeller<vDagen){
...

compares two numbers.

The code improvements in-thread have been discussed absent of a test file so there’s no way to tell if performance has improved even if the code has. The code here would suggest that changes discussed have reduced/removed the slow performance in the OP’s source file.

1 Like

As a developer who’s done a fair amount of date processing - that’s one of the worst conversions to inflict unnecessarily on code. So yeah, I’d expect @mwra’s numeric comparison to have completely eliminated the performance problem.

People often dramatically underestimate how long conversion to and from strings can take when done en-mass. I did a lot of work with geophysical data with up to hundreds of thousands of floating point numbers, read from XML. It wasn’t until a scientist at Geoscience Australia did a formal timed proof that most of those in the project accepted that it was the number parsing that was slow, not the use of XML.

(My developer perspective comment obviously not aimed at @eastgate but it’s surprising how many experienced programmers don’t understand parsing costs.)

1 Like

That mystery has not yet been solved other than the comment from @mwra.

in fact, I’m confused because today my original function only takes 23 seconds at startup to make the calculations. No idea why. I didn’t restart my mac or make any other changes.

Nevertheless I think the function has improved significantly.

I am not a developer but this makes sense.

I think a part of the function is missing here?

    // start end are same day => whole weeks apart 
    vWorkWeekDays = fGetWorkDaysOnly(iStart,iEnd);
    // no remainder work days, so return and exit
    return vWorkWeekDays;
  };

is referring to a sub function “fGetWorkDaysOnly” which I couldn’t find in the file.

I want to further investigate what is causing the changed performance of my original function and will post my findings -if any- here.

Many thanks @mwra. I will adopt your version.

Sadly, yes. That line should read:

floor(days(vOffsetStartDate,iEnd)/7)*5

Fixed in this version of the file: ElaspedWorkDays3.tbx (186.4 KB)

I’ll update the link upthread too.

Interesting! I think active/trained programmers are a minority (even if well represented mote). A wider group as ‘scripters’ (myself included)—able to repurpose code but lacking formal training in the underlying issues, such as the into/out of string conversions you raise.

For non (pro) coders the pinch point is understanding scale. Either more/longer code, or more use of code (e.g. 100s of rules/edicts vs 10s. The power of modern Macs means (unintentionally) inefficient code’s performance is hidden when working with small file. It is only as the code, and number of notes using it, grows that issues tend to creep in. Plus, those least likely to understand the cause are inevitably those most a risk from unexpected slowdowns.

I open to writing some notes for aTbRef of sub-par approaches to avid—at least when scaling up from first use to a mature document.

With @AndyDentPerth’s comments in mind, I took my earlier public holiday test code:

var:list vHolidays = [2023-12-25;2023-12-26;2024-01-01;2024-03-29];
//Set these dates via Displayed Attributes instead
//$StartDate = date("2023-12-21");
//$EndDate = date("2024-01-03");
var:number vCount=0;
vHolidays.each(aDay){
  if(date(aDay)>date($StartDate-"1 day") & date(aDay)<date($EndDate+"1 day")){vCount-=1;};
};
$Text = vCount;

and changed it to:

var:list vHolidays = [date("2023-12-25");date("2023-12-26");date("2024-01-01");date("2024-03-29")];
//Set these dates via Displayed Attributes instead
//$StartDate = date("2023-12-21");
//$EndDate = date("2024-01-03");
var:number vCount=0;
var:interval vInterval = interval("1 day");
vHolidays.each(aDay){
  if(aDay>($StartDate-vInterval) & aDay<($EndDate+vInterval)){vCount-=1;};
};
$Text = vCount;

What changed, and why?

Minimising string to date coercion. This affects the vHolidays List-type variable holding the public holidays. The issue is not where they are stored(here, in a config note, etc.) but that every list item is coerced from String to Date type and every time the .each() is called. Here the loop is called once, but in integrated use it might be called for each of 100s of notes having its workday duration assessed. So, I stored the holiday dates as a date() calls (I don’t think I can ‘manually’ store a Date-type’s value other than as a string. But, this way. once vHolidays is read all further use is iterating Date-type info, instead of calling a date() per list item and again if the .each() loop is run again.

Using Interval type data for in/decrementing dates. Upthread, @eastgate made a passing observation that adding an interval with a value of 1 day is more efficient (at least, if working at scale) than the more normal adding of the string ‘1 day’. See the code above, in the e.each() loop to see the difference. Here we are only using an interval of 1 day so we can store that once and re-use, doing so outside the loop so the variable isn’t remade every loop.

FWIW, the two test can be reviewed in this test doc: Holiday-test1.tbx (104.1 KB). the $Edict of note ‘Test 1’ is the original code, ‘Test 2’ has the optimised(?) code.