caronc / dateblock
Block (sleep) until a cron-style moment in time
Package info
Language:C++
Type:php-ext
Ext name:ext-dateblock
pkg:composer/caronc/dateblock
Fund package maintenance!
Requires
- php: >=8.0
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-30 01:49:49 UTC
README
Dateblock
Dateblock allows you to block (exactly how a Linux/Unix Cron might) until a specific point of time unlike 'sleep' which blocks for a set period of time. The difference is very subtle but can prove to be very useful.
It offers a few new features that the Internet Systems Consortium (ICS) version does not (refering to /etc/crontab style). Dateblock specifically offers the ability to wake/unblock on a specific second and/or drift for certain period afterwards your calculated time is met (kind of like combining sleep and ICS Cron into one).
Python, PHP and Node.js bindings are additionally included, supporting the standard date and time objects of each language!
Quick Start
The dateblock command is a C++ tool; install it from the distribution
packages or build it from source (see Installation).
# When would the next 5th minute start? (--test shows without waiting) dateblock -t -n /5 # Wait for 9:00 in Tokyo, whatever timezone this computer is in dateblock -z Asia/Tokyo -c "0 0 9" # Run a backup at the next quarter hour (dateblock exits with its exit code) dateblock -n /15 -- ./backup.sh --full
The Python library is on PyPI (Linux, macOS and Windows):
pip install dateblock
The PHP extension installs with PIE (Linux and
macOS with a 64-bit time_t, PHP 8.0 and newer):
pie install caronc/dateblock
The Node.js package is on npm (Node.js 20 and newer, on a system with a
64-bit time_t):
npm install @caronc/dateblock
Packages for Fedora, RHEL/CentOS/Alma/Rocky 9 and 10, Debian and Arch Linux can be built from this repository; see Installation.
Syntax: dateblock [options] [--] [command [arguments...]]
Allowed options:
-h [ --help ] Show this help screen.
-t [ --test ] Test (Do not block for the specified period)
-v [ --verbose ] Verbose mode
-i [ --isc ] ISC Mode
-s [ --second ] arg Second (0-59)
-n [ --minute ] arg Minute (0-59)
-o [ --hour ] arg Hour (0-23)
-d [ --dom ] arg Day of Month (1-31)
-m [ --month ] arg Month (1-12 or Jan-Dec)
-w [ --dow ] arg Day of Week (0-6 or Sun-Sat; Sun=0 or 7)
-c [ --cron ] arg Cron string formatting
-x [ --drift ] arg Additional drift time (in seconds).
-z [ --timezone ] arg Timezone to read the schedule in (ie:
America/Toronto); defaults to this computer's.
Anything after the options (or after --) is a command to run once the
time is reached.
A variety of syntax is accepted by this tool such as:
x (Value) where 'x' is represented numerically.
*/x (or /x) (Modulus) where 'x' is represented numerically.
x-y (or y-x) (Range) where 'x' and 'y' are are represented numerically.
x,y (Separator) where 'x' and 'y' are are represented numerically.
Mon-Fri, Jan (Names) the month and the day of the week can be written
as names (Jan-Dec and Sun-Sat, in any case).
7 (Sunday) as in crontab, a day of the week of 7 is Sunday
too, so Fri-7 is Friday to Sunday.
Note: With the exception of the drift option (+), all variations of the syntax mentioned above can be mixed if separated using the 'comma' (Separator) operator. ie: /a,b,c-d,/e is valid. However: x-y-z is not valid, nor is /x/y, /x-y or a '' mixed with other entries (ie: *,5). All values must be within the range of it's time type. Thus 400-4000 would never work since no time constraint even resides within that range.
The --cron(-c) switch allows one to specify the standard cron formatting:
drift time ----------------------------------------------+
day of week (0 - 7) (Sunday=0 or 7) ------------------+ |
month (1 - 12) ------------------------------------+ | |
day of month (1 - 31) --------------------------+ | | |
hour (0 - 23) -------------------------------+ | | | |
min (0 - 59) -----------------------------+ | | | | |
sec (0 - 59) --------------------------- | | | | | |
| | | | | | |
- - - - - - -
* * * * * * *
Substitute an asterisk (*) as a placeholder for arguments you are not interested in. Asterisks are automatically placed in strings missing all 6 arguments (separated by white space).
The crontab shortcuts work too (with or without --isc). Without --isc
one can be followed by a drift (ie: @daily +30):
@hourly the start of every hour
@daily (or @midnight) every day at 00:00
@weekly every Sunday at 00:00
@monthly the 1st of every month at 00:00
@yearly (or @annually) every January 1st at 00:00
The --isc (-i) switch makes the cron interpretation equivalent to what it is today (ISC stands for Internet Systems Consortium). Hence:
ISC Format (no second or drift)
+----------------------------- min (0 - 59)
| +----------------------- hour (0 - 23)
| | +----------------- day of month (1 - 31)
| | | +----------- month (1 - 12)
| | | | +----- day of week (0 - 7) (Sunday=0 or 7)
| | | | |
- - - - -
* * * * *
The --test(-t) switch is your greatest ally. Use this to simply build and test your crons to find out when they would have blocked until without actually blocking. If you pair the the test with the --ics (-i), then you can use this tool to help construct regular crons for /etc/crontab if you're new to the idea and still learning how the crontabs work.
Timezones:
The schedule is read in this computer's timezone unless --timezone
(-z) names another one from the timezone database (such as
America/Toronto or Asia/Tokyo). Daylight saving follows that timezone,
and dateblock unblocks at that exact moment no matter where the computer
is. The times shown by --test and --verbose are in that timezone too.
# 9:00 in Tokyo dateblock -t -z Asia/Tokyo -c "0 0 9" Current Time : 2026-10-24 22:05:00 JST (Sat) Block Until : 2026-10-25 09:00:00 JST (Sun)
Running a command:
Put a command (and its arguments) after the options, and dateblock runs it once the time is reached:
dateblock -n /15 -- ./backup.sh --full dateblock -i -c "0 9 * * 1-5" notify-send "Stand-up"
- dateblock becomes the command (the way
niceandtimeoutdo), so the exit code, signals and output are all the command's own. - The command starts at
--, or at the first argument that isn't one of dateblock's options; everything after that is passed to the command as is (no shell is involved), sodateblock -n 5 ls -lrunsls -l. - With --test (-t) the command is only shown, not run.
- If the schedule is invalid the command is never run (exit code 1). A command that can't be found exits with 127, and one that can't be run with 126 (just like a shell).
To run it again and again, put dateblock in a loop:
while dateblock -n /15; do ./backup.sh; done
Drifting:
Drifting is an option that allows you to adjust the calculated results by some additional time. Lets say you wanted the application to wake up on the 1st minute of each 10 min interval (1, 11, 21, 31, 41, 51). Specifying the cron (minute) '/1' would not work. In fact the cron of '/1' would unblock at every minute. You could however achieve the previous example using a drift value of '60' (seconds) and a cron (minute) entry of '*/10'.
In a way, drifting is a little like executing a cron and then sleeping for an additional period right afterwards. However this is a much more elegant solution with a more precise blocking period.
Some things to consider about drifting:
-
Drifting is always calculated 'after' a specified cron.
-
You can not specify a drifting value on it's own without a cron entry to go with it.
-
A drift value is always specified in seconds.
-
You can not drift longer then the interval calculated by the cron itself. In the event this occurs, only the remainder (modulus) is kept; the rest is is considered overflow and will simply be ignored. Drifting behaves this way to prevent missing a segment of time that would have otherwise been calculated. For example... lets say you wanted to unblock every hour, your cron might look like this:
dateblock --hour=/1. With this cron in mind, you could never add a drift of a value larger then 1 hour minus 1 second (3659 seconds)... Why? Because the --hour entry has already pre-defined the time (slice) bounds of 1, therefore a drift value of the same (or more) would cause the calculation to spill into the next hour. An hour it was pre-calculated to wake on anyway.Let's say you wanted to wake ever 2 hours. Well your cron would look like this
dateblock --hour=/2. If you wanted to add a drift into this picture, you'd have up to 2 hours minus 1 second (7319) to work with.However... if you specify a a cron of '*/10' (which would equate to unblocking every 10 seconds), your drift window is only a maximum of 9. So if you specified a drift of of 11 (seconds), the overflow would leave you with an actual drift of '1' (not 11). If you specified 10, then you would not drift at all (would be the same as not setting anything at all). This is how the modulus works when you overflow the drift value.
-
You can use the shortcut character of a plus (+) inline on a cron to immediately invoke the drift and spare you from writing the extra entries. Hence:
dateblock -c '* */5 +60'is the same as writingdateblock -c '* */5 * * * * 60'Once you invoke the + character however, any entries found afterwards will be treated as an error.
Examples
The following would block until a minute divisible by 10 was reached. Minutes divisible by 10 are: 0,10,20,30,40 and 50.
dateblock -n /10
The following would block until a second is divisible by 5 was reached and only on hours 3 and 4. This can also be written as such:
dateblock -s /5 -o 3,4 # you can rewrite the above like this too: dateblock -c "/5 * 3,4 * * *"
The following would block until the 5 minute of every hour:
dateblock -c "* 5" # you can rewrite the above like this too: dateblock -n 5 dateblock -c "* 5 * * * *"
The following would block until a hour divisible by 5 was reached on the first half of the month as well as the 20th of the month) Hours divisible by 5 are: 0, 5,10,15 and 20.
dateblock -o /5 -d 1-14,20
Drifting using the cron was added in v1.0.0 (but was always available using -x or --drift). The advantage you get from using it inline with the cron string is the ability to chain them with multiple drift times using a comma and or range operator. The modulus one will not work here.
The following will block until the 5:20 minute mark of each hour (20 seconds past)
dateblock -c "* 5 * * * * 20" # you can rewrite the above like this too: dateblock -n 5 -x 20 # You can skip to the drift portion by just denoting a plus (+) as the last # entry specified. This is interpreted as a drift time. dateblock -c "* 5 +20"
The expression is considered invalid if more entries are found after the drift period. However this is still valid:
dateblock -c "* 5 * * * * +20"
DateBlock Python Bindings
The DateBlock python bindings are really easy to use too:
from dateblock import dateblock from datetime import datetime, date, time, timedelta # Returns the date object it will unblock at result = dateblock("* /20", block=False) print("Blocking until %s" % result.strftime("%Y-%m-%d %H:%M:%S")) dateblock("* /20") print("Unblocked at %s" % datetime.now().strftime("%Y-%m-%d %H:%M:%S")) # Perhaps you want to calculate a time relative to somewhere in the past? # No problem; lets get a relative time 4 days ago (from now) relative = datetime.now() - timedelta(days=4) result = dateblock("* /20", ref=relative, block=False) # To use the isc switch with the bindings, just set isc=True (by default # it is off). Below would block for the start of each minute (not second # had you not set the isc flag) result = dateblock("/1", isc=True) # Dateblock supports date, time and epoch time too! # # Date: (yyyy, mm, dd) result = dateblock("/5", ref=date(2017, 4, 22)) # Time: (hh, mm, ss) result = dateblock("/5", ref=time(10, 5, 0)) # Epoch: seconds result = dateblock("/5", ref=13424236) # Timezones: an aware datetime (or time) reads the cron in its own # timezone, and the result comes back in that same timezone. This waits # for 9:00 in Tokyo, whatever timezone this computer is in. from zoneinfo import ZoneInfo tokyo = ZoneInfo("Asia/Tokyo") result = dateblock("0 0 9", ref=datetime.now(tokyo))
A few things worth knowing:
- A naive
ref(or none, adate, epoch ortimedelta) uses this computer's timezone and returns a naivedatetime. - A timezone aware
refuses its own timezone, including its daylight saving rules, and returns an awaredatetimewith the sametzinfo. Blocking always ends at that exact moment. - An invalid cron string raises
SyntaxError; a cron that can never happen (such as February 31st) raisesValueError. - While blocking, other Python threads keep running and Ctrl-C
(
KeyboardInterrupt) works as expected.
DateBlock PHP Bindings
The PHP extension adds a single dateblock() function that works the same
way:
<?php // Returns the moment it will unblock at (a DateTimeImmutable) $result = dateblock('* /20', block: false); echo 'Blocking until ', $result->format('Y-m-d H:i:s'), "\n"; dateblock('* /20'); // Work it out from another moment (a DateTime, DateTimeImmutable or // seconds since the epoch) $result = dateblock('* /20', false, new DateTimeImmutable('-4 days')); $result = dateblock('/5', false, 13424236); // A regular crontab entry: the start of each minute $result = dateblock('* * * * *', isc: true); // Timezones: the cron is read in the timezone of the reference, and the // result comes back in that same timezone. This waits for 9:00 in Tokyo. $tokyo = new DateTimeImmutable('now', new DateTimeZone('Asia/Tokyo')); $result = dateblock('0 0 9', ref: $tokyo);
A few things worth knowing:
- Without a
DateTime/DateTimeImmutablereference, the cron is read in PHP's default timezone (date.timezone, seedate_default_timezone_get()), not the computer's. - An invalid cron string throws a
ValueErrorsaying what is wrong; a cron that can never happen (such as February 31st) throws aRuntimeException. - Signal handlers set with
pcntl_signal()run once it returns; Ctrl-C still ends the script straight away unless a handler is set for it.
DateBlock Node.js Bindings
The Node.js package waits without blocking: dateblock() returns a promise,
and the rest of your program keeps running in the meantime.
import { dateblock, next } from '@caronc/dateblock'; // or, in CommonJS: const { dateblock, next } = require('@caronc/dateblock'); // When is the next 20 minute mark? (a Date; nothing waits) const when = next('* /20'); // Wait for it await dateblock('* /20'); // Work it out from another moment (a Date, or milliseconds since the epoch) next('* /20', { ref: new Date(Date.now() - 4 * 86400000) }); // A regular crontab entry: the start of each minute next('* * * * *', { isc: true }); // Timezones: 9:00 in Tokyo, whatever timezone this computer is in await dateblock('0 0 9', { timeZone: 'Asia/Tokyo' }); // Stop waiting early with an AbortSignal const controller = new AbortController(); dateblock('0 0 9', { signal: controller.signal }).catch(() => {}); controller.abort();
A few things worth knowing:
- Without
timeZone, the cron is read in the same timezone JavaScript'sDateuses. - An invalid cron string throws a
SyntaxErrorsaying what is wrong; a cron that can never happen (such as February 31st), an unknown timezone or a reference too far away throws aRangeError. - TypeScript types are included.
Installation
From PyPI (the Python library)
pip install dateblock
This gives you the Python library on Linux, macOS and Windows (the C++ code
is compiled straight into it, so nothing else is needed). The dateblock
command is the C++ tool; install it from the packages or build it from
source as shown below.
With PIE (the PHP extension)
pie install caronc/dateblock
PIE builds the extension on your computer (the C++ code is compiled straight
into it), so it needs a C++ compiler and the PHP development files (ie:
php-devel or php-dev), and then turns it on (or tells you how).
Like the rest of dateblock, it needs a 64-bit time_t: every 64-bit
system has one, and so does current 32-bit ARM Linux. Older 32-bit systems
can't hold dates past 2038, so the build stops there with an error (see
INSTALL.md).
From npm (the Node.js package)
npm install @caronc/dateblock
npm compiles the C++ code while installing, so it needs a C++ compiler and Python (see node-gyp). npm 12 and newer only run that step once you allow it, so the first time:
npm install-scripts approve @caronc/dateblock npm rebuild @caronc/dateblock
It needs a 64-bit time_t too, so it won't build on older 32-bit systems
(see INSTALL.md).
From Source
Assuming you have a C++ compiler and the standard development tools that usually go with it (make, autoconf, automake and libtool), then the following will install everything for you.
./autogen.sh ./configure make make check sudo make install
This installs:
libdateblock.so(the shared library holding all of the date logic) and its headers ininclude/dateblock/, with adateblock.pcfile forpkg-config.- The
dateblockcommand line tool.
To build the Python bindings from your checkout (this compiles the C++ code straight into the Python module):
pip install .
To build the PHP extension from your checkout (it needs the PHP development
files; the C++ code is compiled straight into it unless you add
--with-libdateblock to use the installed library instead):
cd php phpize ./configure make make test sudo make install
Then turn it on by adding extension=dateblock to your php.ini (or a new
file in its conf.d/ directory).
Building with Docker
Don't want to install the build tools? If you have
Docker (or Podman) and
GNU make, you can build and test everything inside a container straight
from a fresh checkout; no ./configure needed:
# Compile and run all of the tests make docker-build # Build packages (each one is tested while it builds) make docker-rpm # Fedora 44 RPMs make docker-rpm DISTRO=el9 # EL9 RPMs (or DISTRO=el10) make docker-rpm-all # the RPMs for Fedora 44, EL9 and EL10 make docker-deb # Debian 13 .deb packages make docker-arch # Arch Linux packages make docker-all # every one of the above # Build for 64-bit ARM instead (ie: Raspberry Pi); runs under emulation, # so it needs qemu-user-static and is much slower (no Arch Linux image) make docker-rpm ARCH=aarch64 # The Python source package and Linux wheels (needs uv or pipx) make docker-pypi make docker-pypi PYPI_ARCHS="x86_64 aarch64" # Rebuild the man page after editing packaging/man/dateblock.md make docker-man # Open a shell inside the build container to poke around make docker-shell # Clean up: 'make clean' removes dist/ and the other build files in the # checkout; 'make release-clean' also removes the build images make clean make release-clean
- Results are written to
dist/<distro>/(dist/<distro>-aarch64/for ARM,dist/pypi/for Python). - Your checkout is mounted read-only, so the container never changes it.
- Podman is used if Docker isn't installed; set
CONTAINER=podmanto prefer it.
See packaging/README.md for the details on each package format, and INSTALL.md for building from source.