Skip to content
Jakub Puchała edited this page Apr 21, 2017 · 6 revisions

User reference book

This small book presents language to express complex time patterns. If you require to do some repetitive tasks with some specific time dependencies, it's good to look here and find if it can be usable for you. This language had been created by me becouse more serious scheduling tasks can't be handled by CRON based tools directly. Instead there are many 'workarounds' like splitting expressions or coupling time pattern processing inside task itself. Constantly repetitive questions about creating such unprocessable by standard tools expressions make me to build the tool that will resolve this problem. It doesn't mean that CRON - based tools are useless from now, this language can be potentially perfect complementary. Even more, as my intuits says - you could translate every cron expression to equivalent expression but it won't work the opposite direction. So as author, I would like to invite you to look around what this language can bring to you, what kind of problems it can solve. Maybe even you will use it some day!

What kind of problem can be solved by this tool

Every programmer who deals with business requirements had to deal sometimes with tasks that has to be executed with some time requirements, for example at specific day and hour. For very few cases existing tools provides direct way to handle such cases, but there are some that you're not able to solve. For example when you would like to schedule tasks from some specific date until other date.

This language looks a bit like SQL!

As it was intended to looks like! I designed it becouse I recognize that the descriptivity of SQL can be very helpful in terms of readability of expression. And also, it was fun to make it that way :-)

Syntax

Syntax of this language is very simple and consists of few basic parts. Let's see it below: repeat every [numeric partOfDate]s | [partOfDate] where [condition | [operator condition]] start at [start_date] stop at [stop_date]. All part of language will be described to let you know how to construct your own queries.

Reserved words

repeat every where and or not is in not in
start at stop at not case when then else esac between

Repeat at

Allows to chose resolution of time pattern. Becouse you don't always want to move on the timeline by adding the smallest fraction of time, you can chose from supported fractions as seconds, minutes, hours, days, months, years. Each of the resolution can be also modified by adding numeric before fraction so 5 seconds mean 0, 5, 10 and so on... ###Where Where clause is the most complex part of the query that allows you express your time pattern. It checks if reference time fits the conditions. Don't be sad if you don't know what reference time is, it will be described a little bit further.

You should know: Basic operators that logically separate your conditions are and, or.
Arithmetical operators are valid and usable in query. There are +,-,*,/,% arithmetic operators. Logical operators are valid and usable in query. There are >,>=,<,<=,=,not,is,is not. There are some complex operators like case or between operator that will help you form your complex expression.

Arithmetical operators

You can mix operators to form a complex expressions. It's prety allowed to something like 8 + (2 * 5) - 4 + 1. Operators precendence will be properly recognized and expression will be calculated correctly.

Tip: try to avoid excessive expression calculation. If there is value that can be calculated before evaluation starts, it's much better to execute it in native code. Less instructions for VM are always better than more.

Logical operators

There are common operators that can be used by you to make comparsion between values, It shouldn't be mysterious that there are possibilities to create complex expressions like 8 + 4 > (2 + 2) - 10

Complex operators

Complex operators can be very usefull to improve readability of the provided expression. Let's see first operator

case 
    when [expression]
    then [expression | numeric | datetime | string]
    ...
    else [expression | numeric | datetime | string]
esac

Must starts with keyword case and ends with keyword esac. It allows to conditionally performs expression or return specific value when one of the conditions is true. It is allowable to return another expression in score of executing expression.

Returning expression can sometimes improve readablitiy of script.

Another operator to use is:

 expression between expression_min and expression_max

which determine if some expression is greater or equal of expression_min and less than expression_max.

Although there is already so much operators supported there is still lack of supports for unary minus operator.

Functions support

It wouldn't be so usefull to define such patterns without possibility to get some fractions from date and use it in some way. That's why it is possible to use some default functions that extracts some values from reference time. In a way you can call it, you will fast catch it, that mixing them with operators is valid and desirable to achieve required effect. This methods should cover most of user requirements but if he's hungry enough, there is possibility to register it's own function. Let's enumerate them all :)

  • bool IsLastDayOfMonth()

Determine if the reference time is setted to last day of month.

  • bool IsDayOfWeek(long dayOfWeek)

Determine if the reference time is setted to apropiate day of week.

  • bool IsEven(long number)

Determine if passed number is even.

  • bool IsOdd(long number)

Determine if passed number is odd.

  • DateTimeOffset Now()

Gets current time.

  • DateTimeOffset UtcNow()

Gets current UTC time.

  • long GetDay()

Gets reference time day.

  • long GetMonth()

Gets reference time month.

  • long GetYear()

Gets reference time year.

  • long GetSecond()

Gets reference time seconds.

  • long GetMinute()

Gets reference time minute.

  • long GetHour()

Gets reference time hour.

  • long GetWeekOfMonth(string type)

Gets reference time week of month. Allowed types to use are simple and calculated. For further explanation, looks here.

  • long GetDayOfYear()

Gets reference time day of year.

  • long GetDayOfWeek()

Gets reference time day of week.

  • bool IsWorkingDay()

Determine if reference time day is working day (monday to friday).

  • bool EveryNth(int value, string partOfDate)

Determine if last fire occured at least N [part of datetime] ago. Part of datetime can be "second", "minute", "hour" or "day".

Working with library

Becouse this language is not standalone executable, you will need to look how to execute the query or construct the request to get the evaluator. As I point you, this library operates on principle requests. It works that way becouse I recognized that It's not obvious what kind evaluation user would want to receive. One person would like to evaluate script and get date's (next occurences), other would want to transpile it to another language. I just wouldn't want to limit users to build their own evaluators.

Instantiating evaluator

As pointed earlier, to instantiate evaluator, you will need construct specific request. After that, there needs to be instantiated converter of type RdlTimeline<TMethodAggregator>. Let's see and describe what kind of parameters will both needs

    /// <summary>
    ///     Allow user to instantiate properly configured request.
    /// </summary>
    /// <param name="query">User defined query.</param>
    /// <param name="source">Source timezone in which query will be evaluated</param>
    /// <param name="target">Target timezone in which evaluated date will be returned to user.</param>
    /// <param name="debuggable">Should query be debuggable?</param>
    /// <param name="formats">Default formats of date in typed query</param>
    public ConvertionRequest(string query, TimeZoneInfo source, TimeZoneInfo target, bool debuggable = false,
        string[] formats = null)

    /// <summary>
    ///     Instantiate to evaluator converter.
    /// </summary>
    /// <param name="throwOnError">Allow errors to be aggregated or rethrowed immediatelly</param>
    public RdlTimeline(bool throwOnError = false)

after that, let's examine how some internal helper method instantiate request and what response you should expect:

    public static ConvertionResponse<IFireTimeEvaluator> Convert<TMethodsAggregator>(string query)
        where TMethodsAggregator : new()
    {
        var request
            = new ConvertionRequest<TMethodsAggregator>(query, TimeZoneInfo.Local, TimeZoneInfo.Local, false, new[]
            {
                "dd/M/yyyy H:m:s",
                "dd/M/yyyy h:m:s tt",
                "dd.M.yyyy H:m:s",
                "dd.M.yyyy h:m:s tt",
                "yyyy-mm.dd HH:mm:ss",
                "yyyy/mm/dd H:m:s",
                "dd.M.yyyy"
            });

        var timeline = new RdlTimeline<TMethodsAggregator>(false);

        return timeline.Convert(request);
    }

As you see, the request had been defined is ConvertionRequest<TMethodsAggregator> which expects query string, source timezone in which context your query will be evaluated and the destination timezone which specify conversion between timezones. Also there is parameter that determine if your evaluator should produce debugger instructions. Note that this feature is only partially implemented and in current state, shouldn't be used. The last parameter determine how would you specify datetimes in query. After instantiating request, you have to instantiate RdlTimeline<TMethodsAggregator> with parameter that determine if any noticed error in query should throws immediatelly exception or aggregate all occured exceptions. The last part is to call Convert(request) that will try to convert you query into response with executable evaluator inside. Before you can use it, you must checks if your convertion request doesn't fail. All scripting errors (parse errors, query validation errors) are aggregated in response object. So to find it out, only what you need is to check in IReadOnlyCollection<VisitationMessage> Messages { get; } property. If any of the produced message has setted property MessageLevel Level { get; } to MessageLevel.Error, your evaluator won't be instantiated and property IFireTimeEvaluator Output { get; } will be null.

Working with evaluator

It is fairly simply, evaluator will produce output until your query exceed the maximum boundary of your query (define by stop at clause) or your reach the max limit allowed to query (01.01.2100 00:00:00). See below for api.

  • DateTimeOffset? NextFire()

Gets the next evaluated occurence.

  • bool IsSatisfiedBy(DateTimeOffset datetime)

Determine if passed DateTimeOffset fits the where clause.

Registering custom functions

It is sometimes necessary to provide custom way of doing some calculations on timeline or checks additional conditions in a way never provided by standard methods. For such requirements, it is possible to bind user defined functions with currently processed timeline. There are few constraints that have to considered. I will enumerate them now

  • Created class must inherit from TQL.RDL.Converter.DefaultMethodsAggregator

  • Created class cannot have non-default constructor.

  • Each method you would like to bind, must be public and annotated by BindableMethod attribute.

  • Your method must return bool, short, int, long or DateTimeOffset. Methods that returns null value are not allowed.

  • Inject* arguments attributes must be placed before any other arguments. Injected parameters cannot be mixed with non injected parameters

Bad: int DoSomething([InjectReferenceTime] DateTimeOffset param1, int arg, [InjectLastFireTime] DateTimeOffset? param3)

Good: int DoSomething([InjectReferenceTime] DateTimeOffset param1, [InjectLastFireTime] DateTimeOffset? param2, int arg)

Finally, after see constraints, let's show how to bind custom function. Firstly define custom class

class CustomMethodsAggregator : DefaultMethodsAggregator
{
    [BindableMethod]
    public bool MyFirstFilter([InjectReferenceTime] DateTimeOffset referenceTime){
        return true;
    }
}

after that instantiate evaluator like you would normally do

var request
   = new ConvertionRequest<CustomMethodsAggregator>(query, timezone, timezone, false, new[]
   {
       "dd/M/yyyy H:m:s",
       "dd/M/yyyy h:m:s tt",
       "dd.M.yyyy H:m:s",
       "dd.M.yyyy h:m:s tt",
       "yyyy-mm.dd HH:mm:ss",
       "yyyy/mm/dd H:m:s",
       "dd.M.yyyy"
   });
var timeline = new RdlTimeline<CustomMethodsAggregator>(false);
var response = timeline.Convert(request);
return response;

For more details, open this tests.

And that's all, at least from user perspective!

Examples

You can find examples here: evaluator tests

Some important tips

  • Avoid excessive usage of functions with dynamically calculated parameter.

In general, function calls are cached (but there are limitiations!). Becouse of how sliding on the timeline works, it is not necessary to call twice or more the same function. For example where GetDay() in (11,12) and GetDay() in (20,21) won't call GetDay twice. However if you would define your function MyFunction(int myArg) and call it twice with args MyFunction(2 + 3) and nextly MyFunction(3 + 2) there is no any mechanism that will recognize this function has the same value in parameter and can be cached. To see how cache mechanism works, see cache descitpion.

  • How cached function call works

Caching function calls is compile time feature. It merely checks how many times function call with specific arguments occur and if occurs at least twice, it will change first call to call & store. Then each next calls (with parts of code that loads parameters) will be generated as load from memory. With such approach there is such problem that it should traverse the parameters to determine if it's arguments are the same between function calls. There is not such mechanism right now.