When faced with combinatorial problems, it is hard to know where to start. Using a logic engine to exhaust possible solutions is a novel way to approach such problems, and leverages an engine to do the hard work for us.
In this example we will explore specifically how the substr() predicate can be used to generate all possible substrings of a string using rules.
Example of pattern generation
A simple pattern that shows the pattern generation is the substr() predicate:
substr(string, before, length, after, subString)Introducing pattern generation with two simple examples
Notice that there is only one solution for example 1 below, and two solutions for example 2, as the possible solutions for the open ended variables will be generated automatically by the engine.
Code: Example 1 of substr
substr("string", 2, 2, "v:after", "ri")This returns 2, as it has 2 characters before the substring ri, and we use 2 characters of the substring ri.
Code: Example 2 of substr
substr("string", "v:before", 5, "v:after", "v:subString")Now, this query will return two solutions:
- First solution has
before=0andafter=1, andsubString="strin" - Second solution has
before=1andafter=0, andsubString="tring"
Combining the pattern generation with rules
Let's increase the complexity of the solution by adding rules for the allowed solutions and make the string a bit more complex to match against.
Note that values in TerminusDB default to being treated as IRIs, unless specifically typed as specific literals, or that the context makes a specific choice for how to interpret a parameter, such as when supplying a pattern to match to substr().
What we are doing here is matching a string that has a pattern of 8 groups of 4 digits separated by hyphens. We want to get one solution per number as an integer, and know the string positions where that number was found.
By matching on - we can filter out substrings that do not include a hyphen.
Code: Example 3 of substr
select().and(
// Let variable string have the string of numbers
eq("v:string", literal("0000-0001-0002-0003-0004-0005-0006-0007", "xsd:string")),
// Get every possible substring of 4 characters
substr("v:string", "v:start", 4, "v:end", "v:str"),
// Filter out substrings with a hyphen and convert to integer
and(
not().substr("v:str", "v:n_1", "v:n_2", "v:n_3", "-"),
typecast("v:str", "xsd:integer", "v:number")
)
)Here is the result of the logic.
| 0 | 0 | 0000 |
|---|---|---|
| 1 | 5 | 0001 |
| 2 | 10 | 0002 |
| 3 | 15 | 0003 |
| 4 | 20 | 0004 |
| 5 | 25 | 0005 |
| 6 | 30 | 0006 |
| 7 | 35 | 0007 |
Temporal and numeric pattern generation
Beyond string patterns, WOQL provides a rich set of predicates that generate sequences of dates, numbers, and other temporal values. These predicates exploit the same pattern-generation principle: leave a variable unbound and the engine exhausts every valid solution.
Sequence generation
The sequence() predicate generates values in a half-open range [start, end). It supports integers, decimals, doubles, dates, dateTimes, gYearMonth, gYear, time, gMonth, gDay, and gMonthDay. An optional step controls the increment (defaults to 1 in the natural unit of the type).
Generate integers 1 through 5
WOQL.sequence("v:n",
literal(1, "xsd:integer"),
literal(6, "xsd:integer"))Returns five results: n = 1, 2, 3, 4, 5.
Generate dates in January 2025
WOQL.sequence("v:date",
literal("2025-01-01", "xsd:date"),
literal("2025-02-01", "xsd:date"))Returns 31 results, one per day. Each date is an xsd:date value.
Generate every other day with a step
WOQL.sequence("v:date",
literal("2025-01-01", "xsd:date"),
literal("2025-01-10", "xsd:date"),
literal(2, "xsd:integer"))Returns dates: Jan 1, Jan 3, Jan 5, Jan 7, Jan 9.
Generate months in the first half of 2025
WOQL.sequence("v:ym",
literal("2025-01", "xsd:gYearMonth"),
literal("2025-07", "xsd:gYearMonth"))Returns six gYearMonth values: 2025-01 through 2025-06.
Weekday classification
weekday() returns the ISO 8601 day-of-week number (Monday = 1, Sunday = 7). weekday_sunday_start() uses the US convention (Sunday = 1, Saturday = 7).
Get the weekday for a specific date
WOQL.weekday(literal("2025-01-06", "xsd:date"), "v:dow")Returns dow = 1 (Monday).
Combine sequence + weekday to find all Fridays in January
WOQL.and(
WOQL.sequence("v:date",
literal("2025-01-01", "xsd:date"),
literal("2025-02-01", "xsd:date")),
WOQL.weekday("v:date", "v:dow"),
WOQL.eq("v:dow", literal(5, "xsd:integer"))
)Returns every Friday in January 2025.
ISO week number
iso_week() computes the ISO 8601 week-numbering year and week number for a date.
WOQL.iso_week(literal("2025-01-01", "xsd:date"), "v:year", "v:week")Returns year = 2025, week = 1. Note: the ISO year may differ from the calendar year near year boundaries.
Day navigation
day_after() and day_before() compute the next or previous calendar day. Both are bidirectional — give either argument and the engine computes the other.
WOQL.day_after(literal("2025-01-31", "xsd:date"), "v:next")Returns next = 2025-02-01. Month and year boundaries are handled automatically.
WOQL.day_before(literal("2025-03-01", "xsd:date"), "v:prev")Returns prev = 2025-02-28 (or Feb 29 in a leap year).
Month boundary helpers
month_start_date() and month_end_date() convert a gYearMonth to the first or last day of that month.
WOQL.and(
WOQL.month_start_date(literal("2025-02", "xsd:gYearMonth"), "v:first"),
WOQL.month_end_date(literal("2025-02", "xsd:gYearMonth"), "v:last")
)Returns first = 2025-02-01, last = 2025-02-28. Leap years are handled correctly.
month_start_dates() and month_end_dates() are generators that produce every first-of-month or last-of-month date in a range.
WOQL.month_end_dates("v:eom",
literal("2025-01-01", "xsd:date"),
literal("2025-07-01", "xsd:date"))Returns six results: Jan 31, Feb 28, Mar 31, Apr 30, May 31, Jun 30.
Duration arithmetic
date_duration() is tri-directional: given any two of start, end, and duration, it computes the third.
Compute a duration between two dates
WOQL.date_duration(
literal("2025-01-01", "xsd:date"),
literal("2025-04-01", "xsd:date"),
"v:duration")Returns duration = P90D (or equivalent).
Compute an end date from start + duration
WOQL.date_duration(
literal("2025-03-31", "xsd:date"),
"v:end",
literal("P40D", "xsd:duration"))Returns end = 2025-05-10.
Interval construction
interval() constructs or deconstructs an xdd:dateTimeInterval from start and end dates. interval_start_duration() and interval_duration_end() relate an interval to a start or end point and a duration.
WOQL.interval(
literal("2025-01-01", "xsd:date"),
literal("2025-04-01", "xsd:date"),
"v:iv")Returns an xdd:dateTimeInterval value representing Q1 2025.
Allen's interval algebra
interval_relation_typed() classifies the temporal relationship between two intervals using Allen's 13 interval relations (before, meets, overlaps, starts, during, finishes, equals, and their inverses).
WOQL.interval_relation_typed("v:rel",
literal("2025-01-01/2025-04-01", "xdd:dateTimeInterval"),
literal("2025-04-01/2025-07-01", "xdd:dateTimeInterval"))Returns rel = "meets" — the first interval ends exactly where the second begins.
interval_relation() does the same on raw start/end pairs without constructing interval values.
Range comparison
range_min() and range_max() find the minimum or maximum value in a list. They work with any comparable type: numbers, dates, strings.
WOQL.and(
WOQL.range_min([
literal("2025-05-10", "xsd:date"),
literal("2025-05-05", "xsd:date"),
literal("2025-05-15", "xsd:date")
], "v:earliest"),
WOQL.range_max([
literal("2025-05-10", "xsd:date"),
literal("2025-05-05", "xsd:date"),
literal("2025-05-15", "xsd:date")
], "v:latest")
)Returns earliest = 2025-05-05, latest = 2025-05-15.
Range checking
in_range() tests whether a value falls within a half-open range [start, end).
WOQL.in_range(
literal("2025-03-15", "xsd:date"),
literal("2025-01-01", "xsd:date"),
literal("2025-04-01", "xsd:date"))Succeeds (one result) because March 15 is within Q1.
Composing patterns
The real power comes from combining these predicates. Here is a composed pattern that finds the last business day of each month in H1 2025:
WOQL.and(
WOQL.month_end_dates("v:eom",
literal("2025-01-01", "xsd:date"),
literal("2025-07-01", "xsd:date")),
WOQL.weekday("v:eom", "v:dow"),
WOQL.lte("v:dow", 5)
)month_end_dates generates month-end dates, weekday classifies each, and lte filters to weekdays only (Mon-Fri). Months whose last day is a Saturday or Sunday are excluded — combine with day_before to step backward to the preceding Friday if needed.
Conclusion
The examples show how to use pattern generation to match against string patterns and extract values from them, as well as how to generate and compose temporal sequences, date arithmetic, interval algebra, and range operations. Every possible solution is generated automatically by the engine to match against.