Mixing data types
Last updated
Last updated
While drafting clauses in Clause9 is certainly not “programming” in the traditional sense, Clause9 borrows some best-practices from traditional programming languages. Legal experts who have never done any traditional programming are often confused by these practices.
One such issue is the concept of data types — aka the issue of not mixing apples and oranges. This page deliberately takes a deep-dive into dealing with these types, to fully explain how they work in Clause9.
A first important idea to grasp is the distinction between natural language mode and conditions/calculations mode.
The standard mode in Clause9 is the natural language mode. This is the mode you will implicitly arrive in when starting to type something in a clause. It will consist of “human” language stuff, such as words in English/French/Dutch, punctuation and numbering, etc. For example, the following clause entirely consists of the natural language mode elements:
A clause that consists entirely of natural language mode elements will be completely static, i.e. aside from the numbering and formatting of the clause, this clause will not change appearance. To insert dynamic elements, you insert conditions and calculations, for example:
As soon as you type in an opening accolade {
you will leave the natural language mode, and enter either
the condition mode, for which the result’s type is always true or false); or
the calculation mode (for which the result’s type depends on the kind of calculation being made).
In the example above, #contract^value < 500 EUR
and #contract^value < 2000 EUR
are written in the condition mode, while apples^amount * #apples^unit-price
is written in the calculation mode.
After the closing accolade }
you will in any case leave these modes, and return to the natural language mode. However, the switch between the modes is more complex than just looking at the opening and closing accolade. For example, in paragraph 1. above, the parts between the colon (:
) and the vertical bar (|
) are once again in the natural language mode, i.e. the words low and medium and high.
As the name implies, calculation mode expects to perform calculations. The input to these calculations consists of a mix of countable elements and optional operators — e.g. 5 + 24
or 5 * 4.5 EUR
, but also 12 months * 4
or 2020_11_23 + 3 days
, because you can perform interesting calculations with durations and dates.
Instead of using directly countable elements, you can also work with elements that ultimately boil down to numbers. For example, in apples^amount * #apples^unit-price
, the datafields “amount
” and “unit-price
” can be assumed to contain countable things. When, for example, the number 5 would be assigned to apples^amount
and the #apples^unit-price
would be assigned the value 0.4 EUR, Clause9 will happily perform the calculation for you and return the result (2 EUR).
Note that natural text cannot, as such, be used in calculation mode. After all, text is — as such such — not countable. It does not make sense to, for example, perform a calculation to divide the words “the Buyer buys an apple” by the words “the Seller sells a pear”.
Writing in the condition mode will be writing in calculation mode, with one exception. Condition mode is meant to ultimately result in true or false, and will therefore contain a left side with a calculation, a comparison operator and a right side with yet another calculation.
For example, in the condition apples^amount * #apples^unit-price < 2000 EUR
, you will see a left side (containing a calculation on apples), a comparison-operator (smaller than <), and a right side (with a single currency-value).
In Clause9, as in most programming languages, you can only calculate with elements of the same data type, and you can only compare elements of the same data type. For example, it does not make sense to add 25 EUR to a date, or to compare the text “alpha” to the duration of 5 months.
Clause9 offers the following basic data types:
In addition, the following special data types are also offered.
The three different modes result in three different data types:
Clause9 currently supports the following currencies:
While it is not possible to perform calculations or comparisons with elements of different data types, it is possible to convert an element to another data type. This conversion can either be done explicitly or implicitly.
Explicit conversions can be achieved using special functions. For example, the special function @extract-number
will take a currency number (e.g., 5.5 EUR
) and return the floating point number (5.5
). Similarly, the sole purpose of the special function @str
is to convert the argument passed to it into text — e.g. @str(3 months)
will, depending on the styling settings, result in the text 3 months
when the active language is English.
At first glance, this may look like nothing changed; under the hood, however, the type of this element changed from a duration to a text, meaning that the operations that can be applied to a duration element (e.g., addition or subtraction) will suddenly no longer work, while the operations that can be applied to a text element (e.g., capitalization) will suddenly become possible.
If the active language is not English, the conversion will be much more visible. For example, @str(3 months)
may also be converted to “trois (3) mois” in French, with a styling setting where numbers are always converted to both letters and numbers.
Most special functions will perform something more than solely converting an argument from one data type to another data type. Examples:
@round
and @floor
will convert the floating point number passed to them, and then either round that number or truncate the decimal part, returning a whole number
@comma-split
will take a text element and return a list element that contains individual text elements (e.g. @comma-split("alpha, beta, gamma")
will return a list with text elements "alpha"
, "beta"
and "gamma"
.
It would be cumbersome to always have to explicitly convert data types — e.g., imagine that in order to take 5.5 times the duration of a contract, you would have to write 5.5 * @float(@months-in(#contract^duration))
The underlying reasoning is as follows: you are performing a multiplication of a floating point number and a duration, which is not possible. There exists a special function @months-in
that takes a duration and returns a whole number that corresponds to the amount of months in that duration. However, you would then have to perform yet another conversion from this whole number to the floating point number. You would therefore have to use the hypothetical @float
function.
To avoid these situations, Clause9 will implicitly perform conversions for you when no ambiguity is involved:
(*) When mixing a date with a duration, note that the order and operation matters. The conversion will only be applied with date +/- duration
. If you change the ordering (e.g., duration first) or the operation (e.g. multiplication), the result will be an error.
(**) When mixing a list with, the order and operation matters: the conversion will only be applied with list +/- element
, not with element +/- list
.
When using the -
operator, the element(s) at the right side are removed from the list at the left side. For example, @list("alpha", "beta") - "alpha"
and @list("alpha", "beta") - @list("alpha")
both result in @list("beta")
.
When using the +
operator, the result depends on the right-side:
If both the left & right side are lists, then the result will be a concatenation of the two lists, with all the elements of the right list appended to the left list, irrespective of whether they were already present in the left list. For example, @list("alpha", "beta", "gamma") + @list("alpha", "delta")
will result in @list("alpha", "beta", "gamma", "alpha", "delta")
. (If you want a union of the list, where elements-already-present are not added again, use the @union
special function instead of +).
If the right side is not a list, then it will be added as a new element of the left list, irrespective of whether the right side element was already present in the list. For example, @list("alpha", "beta") + "alpha"
will result in @list("alpha", "beta", "alpha")
. (If you want a union of the list and the single element, where the right-side element is not added again to the left list, use the @union
special function instead of +).
When using datafields, values will often not be present, because they have not (yet) been filled in by the end-user. Within a calculation/condition, to avoid errors, Clause9 will try to convert this undefined value to a sensible default value:
Note that the date field is missing from the table above: an undefined will never be implicitly converted to a date field. (After all, there exists no date that would make sense here: what could possibly be considered “date zero”?)
Tip: when you do not like these implicit conversions of unavailable values inside a calculation, you may want to use an exclamation point after the hash.
For example, while you may be happy with {#apples^amount * 0.2 EUR}
resulting in 0 EUR when the amount of apples has not yet been completed by the end-user, it may equally be the case that you absolutely need the user to fill in this amount. If so, then use {#apples^!amount * 0.2 EUR}
instead — this will result in a box that invites the user to fill in this value, signaling that the calculation simply cannot be completed without a value assigned to the amount of apples.
In condition mode, the following implicit conversions are carried out to convert a data type to true/false:
Note that in an if-then-else condition, you can omit the operator and right-hand side of the comparison for the sake of brevity; Clause9 will then automatically insert = true
. For example:
Instead of writing {#contract^is-dutch-law = true: ... }
you can also write {#contract^is-dutch-law: ... }
. If the datafield is-dutch-law
is a true/false datafield, then no implicit conversions will occur.
Instead of writing {#contract^value > 0: ... }
you can also write {#contract^value = true: ... }
or the shorter version{#contract^value: ... }
. In both of these cases, the contract-value
whole number datafield will be converted into a true/false, meaning it will result in true if a non-zero value is assigned to this datafield; it will instead result in false when no value is assigned at all, or when the value that is assigned to this datafield, is equal to zero.
When combining duration elements of different units in a calculation, conversions may be necessary — e.g., when you add 2 days
to 3 months
. You should be aware that such conversions will in many cases result in rounding errors and unexpected results, because a duration does not contain any context about the month or year it relates to.
Clause9 will convert durations with different units into the lowest time unit, e.g. year + months will be converted to months, year + days will be converted into days, and months + weeks will be converted into weeks. Clause9 tries to use sensible averages during this conversion, but unexpected results are bound to happen due to the lack of context.
The bottom line is that you should try to avoid these implicit conversions as much as possible.
For example, Clause9 happens to convert 27 months + 1 day
to 822 days, because it first converts 27 months to two years (= 730 days) and 3 months (= 3 times average of 30.417 days, so 91 days), and then adds yet another day. An equally sensible result would, however, be 811 days (= 27 * 30 days + 1 day).
Similarly, Clause9 happens to convert a month into 4.345 weeks, so that 3 months + 1 week
gets converted to 14 weeks. Other software or people would perhaps answer 13 weeks.
Clause9 allows you insert a datafield directly into natural text. For example:
While perhaps not immediately obvious, this is also a situation where apples (clause text) are combined with oranges (number datafield). To avoid that you would have to insert all kinds of ugly codes to allow this, Clause9 will implicitly convert this datafield into clause text for you.
In most situations, this conversion will be very straightforward. However, do note the following.
A (whole/floating point/currency) number and a date value be converted into text in accordance with the locale styling rules (e.g., for the decimals style, currency symbol placement, short/long date format, etc.).
A true/false value will get translated — e.g., Alpha {true}.
will be converted into Alpha true
in English and Alpha vrai
in French.
A list value will have its elements printed consequentially with spaces in between — e.g. @list("alpha", "beta", "gamma")
will be converted into alpha beta gamma
. If you want to have comma’s plus and/or etc. in such list, or perhaps convert it into a bullet-list, then you need to use one of the special functions — e.g., @enumerate-andor(@list("alpha", "beta", "gamma"))
.
Some conversions are deliberately not performed, because they would be ambiguous:
Adding a (whole/floating/currency) number to a duration or date. For example, 3 months + 3
results in an error because it will be difficult for the software to figure out whether you want the result to be a duration (6 months) or a whole number instead (6).
Adding a number to a date. For example, 2020_04_07 + 3
results in an error, because it is not clear whether you would like to add 3 years, 3 months, 3 weeks or 3 days instead.
Other conversions are deliberately not performed because they make no sense:
Mixing text and whole/floating/currency numbers. It will usually be obvious that this does not make sense — e.g. "alpha" + 6
— but this may not always be intuitive to those new to Clause9.
For example, if you create a text datafield #contract^value
, then Clause9 will treat the contents of this datafield as text, and issue an error when you try to do a calculation such as #contract^value * 2
. This error will make perfect sense when the end-user would type in “real” text (such as the word “Paris”), because you can obviously not multiply Paris by two. However, when an end-user would type in a number (e.g., 3500), then it will not be so obvious why the software is complaining.
While not a data type conversion issue, you should also note that some operations are not allowed between elements of the same data type:
You cannot mix different currencies — e.g., adding {200 EUR + 300 USD}
results in an error.
You cannot add dates to each other — e.g. {2020_04_03 + 2020_05_3}
results in an invalid operation error.
An if/then/else construction will normally result in clause text. For example,
The contents of the internal snippet CONTRACT will be clause text, because its sole element is an if/then/else construction between curly braces. Such clause text can be inserted into the main body of the clause without any conversion being necessary.
However, it is also possible to use the if/then/else construction to return other data values. You can do this by wrapping the content of the if/then/else after the colon (:) between curly braces. For example:
It is interesting to analyse why you will get a “value is not a number” error when you omit the two pairs of curly braces in the if/then/else construction:
The reason is that Clause9 will do the following:
When encountering the {@INTEREST * 1.5}
, it will replace the @SNIPPET with the contents of the internal snippet, arriving at {#contract^jurisdiction = "dutch": 0.05 * #contract^value | else: 0.08 * #contract^value } * 1.5
It will then execute the if/then/else construction, and check whether the contract’s jurisdiction is equal to “dutch”. Assuming this is indeed the case, Clause9 will notice that the associated fragment it has to insert into the main part of the clause is 0.05 * #contract^value
.
Because this fragment is not wrapped in curly braces, Clause9 will treat the fragment as clause text. For example, if the contract’s value is currently set to 1000, and the current styling happens to be metric, it would convert the fragment into the text “0.05 * 1.000” . Notice that:
because the fragment is treated as clause text, Clause9 will treat the 0.05 literally, i.e. not consider it to be a number. Accordingly, despite the fact that the styling settings are set to metric (so that a floating point number such as 0.05 would be printed with a comma as “0,05”), Clause9 will print it as “0.05”
the contract’s value (1000) is a whole number that is inserted into clause text, and will therefore get converted to clause text, respecting all the styling settings (such as the metric style’s dot between the 1 and the three zeroes)
Next, it will try to multiply the clause text “0.05 * 1.000” by the number 1.5, which obviously does not make sense because you cannot multiply text by a number. It will therefore issue a warning “value is not a number”.
Pursuant to article 3§4 of the Civil Code, #Buyer shall ...
Why is shall pay 10.20 EUR
printed as “shall pay 10.20 EUR”, while my styling settings dictate metric style with a euro-symbol?
Why do I see an error message with Pursuant to article 3§4 of the Civil Code, #Buyer shall ...
?
I get an “incompatible argument types” error when using the following clause:
#Buyer shall pay interest equal to @days-between(#contract^start-date, @today)
The software does not accept the following clause (shows a yellow error):
#Buyer shall pay interest equal to @days-between({#contract^prolonged: #contract^prolongation-date | else: #contract^start-date}, @today)
Data type | Examples |
---|---|
Data type | Example |
---|---|
Mode | Result |
---|---|
Currency | Symbol used in Clause9 |
---|---|
Data type 1 | Data type 2 | Resulting data type | Example |
---|---|---|---|