Git Product home page Git Product logo

js-schyntax's Introduction

Schyntax is a domain-specific language for defining event schedules in a terse, but readable, format. For example, if you want something to run every five minutes, you could write min(*%5).

This project holds the language documentation and official test suite. The reference implementation is C# Schyntax. The is also a fully-compatible JavaScript implementation.

If you're interested in porting Schyntax to another language, please read this.

Syntax

Format strings are composed of groups and expressions.

General Syntax Rules:

  • Expression uses a similar syntax as function calls in C-Style languages: name(arg0, arg1, arg2). The commas between arguments are optional.
  • Format strings are case-insensitive. dayofmonth is equivalent to DAYOFMONTH or dayOfMonth, etc.
  • All whitespace is insignificant.
  • Fractional numbers are not supported in any expression.
  • An argument preceded by a ! is treated as an exclude. days(!sat..sun) means that Saturday through Sunday are excluded from the schedule.
  • All expressions accept any number of arguments, and may mix includes and excludes. For example, you might specify every weekday except tuesday as days(mon..fri, !tues).
  • All time-related values are evaluated as UTC. hours(12) will be noon UTC, not noon local.
  • Numeric ranges are specified in the form of start..end. For example, days(1..5) is the first five days of the month. The order of start and end is significant, and in cases where start > end it will be interpreted as a range which wraps. In other words, minutes(58..2) means it will run on minutes 58, 59, 0, 1, and 2.
  • The .. operator is inclusive of the start and end values. 1..4 is equal to 1,2,3,4. There is also a ..< operator which is the "half-open" range operator, meaning it is inclusive of the start, but exclusive of the end value. 1..<4 is equal to 1,2,3.
  • The wildcard * operator means "any value." For example, min(*) means "run every minute."
  • The % operator can be used to define intervals. seconds(*%2) will run on all even seconds. seconds(7%3) will run at 7,10,13, ...,55, and 58 seconds of every minute. seconds(7..19%4) will run at 7,11,15, and 19 seconds. seconds(57..4%2) will run at 57,59,1, and 3 seconds. Note that the interval operation is always relative to the start value in the range.

Expressions

Expressions allow you to define when you want events to occur or when you explicitly do not want them to occur. If your format string does not contain any expressions, it will be invalid and schyntax will throw an exception.

seconds

Aliases: s, sec, second, secondOfMinute, secondsOfMinute

Accepts numbers and numeric-range arguments between 0 and 59 inclusive.

minutes

Aliases: m, min, minute, minuteofhour, minutesOfHour

Accepts numbers and numeric-range arguments between 0 and 59 inclusive.

hours

Aliases: h, hour, hourOfDay, hoursOfDay

Accepts numbers and numeric-range arguments between 0 and 23 inclusive.

daysOfWeek

Aliases: day, days, dayOfWeek, dow

Accepts numbers and numeric-range arguments between 1 (Sunday) and 7 (Saturday) inclusive. Additionally, you may use textual days. Two or three-character abbreviations are accepted (such as mo..th or mon..thu) as well as full names (monday..thursday). Because tues, thur, and thurs are common abbreviations, those special cases are also accepted, but it may be better to stick to the more predictable 2-3 characters.

daysOfMonth

Aliases: dom, dayOfMonth

Accepts numbers and numeric-range arguments between 1 and 31 inclusive. A second range from -31 to -1 is also allowed and are counted as days from the end of the month.

Examples:

  • dom(-1) The last day of the month.
  • dom(-5..-1) the last five days of the month.
  • dom(10..-1) The 10th through the last day of the month.

daysOfYear

Aliases: doy, dayOfYear

Accepts numbers and numeric-range arguments between 1 and 366 inclusive. A second range from -366 to -1 is also allowed and are counted as days from the end of the year.

Examples:

  • doy(-1) The last day of the year.
  • doy(-5..-1) the last five days of the year.
  • doy(10..-1) January 10th through December 31st.

dates

Aliases: date

Allows you to include or exclude specific dates or date ranges. Date arguments take the form of m/d. Date ranges are m/d..m/d and are allowed to span across January 1st. If you need to specify a specific year (generally a bad idea), you can use yyyy/m/d. Years 1900 through 2200 are supported.

Examples:

  • dates(!12/25, !7/4) Run every day except December 25th and July 4th.
  • dates(4/1) Only run on April 1st.
  • dates(4/1 .. 4/30) Only run for the month of April.
  • dates(4/1 .. 4/30, !4/16) Run every day in April except for April 16th.
  • dates(!12/25 .. 1/1) Run every day except December 25th through January 1st.

Defaults

When a format string does not include all expression types, some assumptions must be made about the missing values. Schyntax looks at the expression with the highest-resolution, and then sets exp_name(0) for any higher-resolution expressions. For example, if hours is the highest resolution specified, then minutes(0) seconds(0) is implicitly added to the format. All day-level expressions (daysOfWeek, daysOfMonth, daysOfYear, dates) are treated as the same resolution. Any other (lower-resolution) missing expression types are considered wildcards, meaning they will match any date/time (equivalent to exp_name(*)).

Here are some examples which illustrate these defaults:

  • minutes(10) will run at ten minutes after the top of every hour on every day.
  • hours(12) will run at noon UTC everyday.
  • daysOfWeek(mon..fri) will run at midnight UTC Mondays through Fridays.
  • daysOfWeek(mon) hours(12) will run at noon UTC on Mondays.
  • daysOfWeek(mon) minutes(0, 30) will run at the top and half of every hour on Mondays.
  • daysOfYear(*) will run at midnight (00:00:00) every day.

Groups

Expressions can be grouped using the { expression, expression, ... } syntax (commas between the expressions are optional). This allows you to setup sets of expressions which are evaluated independently from each other. For example, you may want to have a different set of rules for weekdays vs. weekends.

Examples:

  • {hours(10), days(!sat..sun)} {hours(12), days(sat..sun)} Runs 10:00 on weekdays, and noon on weekends.
  • {dates(10/1 .. 3/31) hours(12)} {dates(4/1 .. 9/30) hours(14)} Runs 12:00 during October through March, and at 14:00 during April through September.

All groups are evaluated to find the next or previous applicable date, and they return which ever date which is closest. All expressions not inside a {} are collected and implicitly put into a group.

Nesting of groups is not allowed.

Want to Help?

There are already two implementations, but there are several ways to contribute if you're interested.

Bug fixes are always welcome. If possible, try to submit a test with your bug fix. If you would like to contribute features to the schyntax expression language, open an issue on this project with your proposed functionality, syntax changes, and use cases BEFORE you submit a pull request to any of the implementations so that it can be discussed.

js-schyntax's People

Contributors

bretcope avatar rossipedia avatar spiralis avatar

Stargazers

 avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

js-schyntax's Issues

Weeks Expressions

Weeks

I'm starting to think there needs to be a weeks expression to help express things like:

  • The first and third Friday of the month.
  • The last Monday of the month.

The two above examples could be expressed as:

days(friday) weeks(1,3) and (monday) weeks(-1)

Just like days of month, the week number could be expressed either as the number of weeks from the start of the month, or a negative number representing the number of weeks from the end of the month (1 and -1-indexed respectively).

Technically this is just syntactic sugar since both of the above examples could already be expressed as:

days(friday) dom(1..7, 15..21) and days(monday) dom(-7..-1)

but the weeks expression is far more readable and less error prone. It should also be rather trivial to implement.

Weeks should have the aliases: w, week, weeks, weekofmonth, and weeksofmonth. Anticipating this expression was actually the reason for commit 1606847 a couple months ago.


Full Weeks

What would be more than simply syntax sugar would be to add a fullWeeks expression. Say you want something to run everyday for the first full week of a month, not simply the first seven days

fullweeks(1)

That would run everyday at midnight for seven days starting on the first Sunday of each month.

I can't think of any way to express this with the existing syntax so I think it would be a good addition to the language and still relatively trivial to implement.

Negative numbers would also be supported:

fullweeks(-1) would run for seven days ending on the last Saturday of the month.

Zero and negative zero would also be supported:

fullweeks(0) would run any day of the month which is before the first Sunday, and fullweeks(-0) would run any days of the month after the final Saturday.

Aliases: fw, fullweek, fullweeks, fullweekofmonth, and fullweeksofmonth.

Package for Bower

There's no reason js-schyntax and js-schtick couldn't be packaged for use in the browser.

Use * for wildcard instead of empty expression

Right now, the easiest way to run once an hour is to say hours(), but this has readability issues, whereas hours(*) would probably make more sense to someone glancing at it. Do let's do that and deprecate the empty expression format. This should also make things like hours(*, !6) easy to write.

Range Operator

Background and Problem

The current range operator is the minus (-) sign. I chose this because that's generally how we write ranges. "I'll have that done in 6-8 weeks." Cron also uses - as its range operator, but I really couldn't care less what cron uses.

The down side of this operator choice is that it means the - is overloaded because dayofmonth supports negative numbers (for counting backwards from the last day of the month). If you want to specify a range using negative numbers, the syntax can look a little ugly.

dom(-5--1) or you could write it a little nicer as dom(-5 - -1), but I concede that it's still not great.

Proposed Solution

It was pointed out to me by someone who likes Ruby that if the range operator was .., it wouldn't require any overloading and might look nicer. dom(-5..-1). I had an initially negative visceral reaction for two reasons. 1. I hate Ruby, and 2. that's not how I write ranges in any context.

The first reason isn't a good one, but the second is. So here's my stance: I am not going to entertain the idea of eliminating the minus sign as a range operator. Yes, it will remain overloaded in this spec. What I am willing to entertain is adding .. as an alias for the range operator, and _only_ the two-dot version... three-dot non-inclusive nonsense would serve no purpose except to confuse people.

My philosophy with the language is that you shouldn't have to remember a lot of syntax because the first time you see it, it should already feel familiar. The way we write ranges 3-5, the way we write dates 4/16, the way we use % as a modulus operator in other languages, etc. It's also the reason there are several aliases for most of the expressions (you don't have to remember whether it's minute or minutes because both work). I don't want you to have to consult the documentation every time you write a schedule. The limit to that flexibility is that it still has to be terse, unambiguous and not misleading.

I think adding an alias for the range fits in with that philosophy, so I don't have any good reasons to object to it.

Compiler Impact

The current tokenizer assumes that all operators are one character, but that's not a huge change.

The .. wouldn't be an alias for the literal - since the minus sign has two meanings, and .. would only be an alias for one meaning. Therefore, we have to implement the operator at the compile step. This should be as simple as altering ScheduleBuilder.js line 182 to read:

if (t.value === '-' || t.value === '..')

Discussion?

Mixin Syntax

I'm thinking about eliminating groups in favor of reusable mixins. The syntax might look something like this:

$weekdays = days(mon-fri)
$noon = hours(12)
$weekdaysAtNoon = { $weekdays $noon }
$weekdaysAtMidnight = { $weekdays hours(0) }

Tokens beginning with $ would be treated as mixins. Attempting to reassign a mixin should throw a syntax error (they can be overridden, however - described later). Since they can't be reassigned, it means we can make them order-independent, so you could use a mixin before it is defined and it will still have the correct value.

As with the rest of the language, whitespace would be insignificant. Mixin identifiers would be case-insensitive. Comma separators would be optional inside the {} since they feel natural, but the syntax is unambiguous without them. The braces would be optional when the mixin only consists of one expression.

The compiler would simply expand the mixins (recursively, as necessary) in order to flatten out the structure. The impact on the existing compiler code should be minimal.


The question becomes, how does this play with the definition of the actual schedule you want sch to use? I see a few possibilities, but here's what I'm leaning towards:

sch should look for a schedule in the following order:

  1. Any free floating schedule (i.e. min(0%2)). This is how sch already works, and it seems like it will remain the most common use case - no reason to break that. There can only be one free-floating schedule (m(0) $midnight = { hours(0) } sec(0) should be invalid syntax).
  2. Look for a "use" statement. There can be either a free floating schedule or a use statement. If both are present, a syntax error should be thrown. Examples:
    • use min(0)
    • use $mixinName
    • use { min(0) $mixinName }
  3. If none of the above conditions are met, a syntax error is thrown.

The use statement is technically redundant, but I think it improves the readability of a schedule which may be cluttered with mixin definitions. We could consider enforcing the use of "use" if a schedule string contains mixin definitions, but that seems unnecessary.

I would like sch to have a .mixin property where global mixins could be defined. Sch could actually ship with a default value for that property with some helpful mixins, but it would still allow users to override, or add to it, with their own mixins.

The sch function itself could accept a second optional argument sch( schedule, [mixins] ). This would allow users a high degree of flexibility in defining their mixins without screwing up the global defaults for everything else.

Mixins cannot be reassigned (they're not variables), but they can be overridden based on order of precedence.

  1. Any mixins defined in the schedule string.
  2. mixins argument.
  3. sch.mixins global property.

For example, if sch.mixins defines $a=h(12) and the mixins argument redefines it as $a=h(14) then any reference to $a in the schedule string will expand to h(14), but any reference to $a in sch.mixins will still expand to the original h(12). This is to prevent unintended consequences in composite mixins. You could think of it as inheriting parent scopes - you can redefine a variable in an inner scope without it affecting the outer scope.


If anyone has any thoughts (@rossipedia in particular), let's discuss them here.

Road to 1.0

I've been doing a lot of thinking lately about what else the language needs in order to be powerful enough to express most schedule requirements people have. The language is already fairly powerful. The best thing it has going for it is its terse, but still readable, syntax which feels familiar the first time you see it. This needs to continue to be the guiding philosophy for additions to the language.

The language does have some flaws and missing features I would like corrected before I start trying to bring attention to it. Items will be crossed out once they're accomplished.

  • Change Range Operator
  • Add "weeks" and "fullweeks" expressions
  • Mixins
  • Decide on "standard library" of mixins and define whether they can have dynamic behavior.
  • Support year in date format y/m/d.
  • Approximate date matching (imagine you want to run on Mondays, unless it's a holiday, then run on the next weekday which is not a holiday).
  • Make expression and argument order significant (substantial change to compiler and search algorithm, but I think it's actually important, and will explain why when I make an issue for it).
  • The language itself is being renamed from sch to schyntax. This will be accompanied by a few other changes:
    • I've started a new github Schyntax organization where all related projects will go, including this project and schtick.
    • This project will be renamed to js-schyntax. Other officially sanctioned implementations will follow a similar naming scheme (i.e. go-schyntax, cpp-schyntax, etc).
    • The JavaScript implementation should be packaged for both npm and bower in order to support both node.js and client-side. It will still be published as schyntax on bower and npm, not js-schyntax.
    • A schyntax project will hold the spec itself. This should contain the grammar definition for the language. documentation, and the official test suite.
    • schyntax.com will host the documentation, probably in the form of Github Pages hosted out of the schyntax project. It should contain also a playground for people to experiment with schyntax.
  • We should create a native utility program and tout it as an alternative to cron itself. We should push to get this included in package managers like yum and apt. It should probably just be called schyntax to improve discoverability with the language itself.
  • Use * for wildcard instead of empty parameter list.

I'd like to make decisions on all of these issues before anyone starts working on a second implementation.

Support for seconds (possibly milliseconds)

Seconds

Proposed keywords: s, sec, seconds, secondofminute, secondsofminute (for consistency).

This would allow describing a repeating task at sub-second resolution, rather than the default smallest unit being minutes, which could be useful if you wanted a repeating task say on the minute and at :30 seconds (twice a minute).

Also, milliseconds could possibly be useful, although when you need sub-1s resolution, I'm not sure that aligning the events to a specific millisecond is really useful.

What do you think?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.