This section contains example OCL rules.
Description
This section contains example OCL rules. They are copied from internal material originally used by XMLdation's team while learning OCL. The rules here tend to be the most complicated and longest to write, which is why they were put into a single document in the first place, so don't worry if the rule are hard to understand or seem complicated.
The purpose is to give the reader an idea how certain types of rules are written in OCL. Edits to rules are most likely needed when copying them to other situations.
List of rules
--Control (GrpHdr) sum check. Please note that using types inside code ignores context otherwise.
--This is why this check can only be used in Group Lvl, as it takes every Amt in the file
self.CtrlSum->size() = 1 implies
self.CtrlSum.oclAsType(decimal) =
(
CreditTransferTransactionInformation10.allInstances()->Amt.InstdAmt->sum().oclAsType(decimal) +
CreditTransferTransactionInformation10.allInstances()->Amt.EqvtAmt.Amt->sum().oclAsType(decimal)
)
--Sumcheck for 2nd lvl
self.CtrlSum->size() = 1 implies self.CtrlSum.oclAsType(decimal) =
self.CdtTrfTxInf.Amt.InstdAmt->sum().oclAsType(decimal) +
self.CdtTrfTxInf.Amt.EqvtAmt.Amt->sum().oclAsType(decimal)
--Element is mandatory
self.ChrgBr->size() = 1
--Element length
self.Cdtr.Nm->size() = 1 implies self.Cdtr.Nm.size() <= 70
--Either ‘Structured’ or ‘Unstructured’ may be present.
not( self.Strd->size()= 1 and self.Ustrd->size()= 1 )
--Or
self.Strd->size()= 1 implies self.Ustrd->size()= 0
--Or
self.Strd->size()= 1 and self.Ustrd->size()= 1 implies false
-- Amount between certain values
self.Amt.InstdAmt > 0.00 and self.Amt.InstdAmt < 1000000000.00
-- Element value if given
self.CdtrRefInf.Tp->size() = 1 implies self.CdtrRefInf.Tp.CdOrPrtry.Cd = "SCOR"
-- Regular expression example
self.CdtrSchmeId.Id.PrvtId.Othr->size() >= 1 implies
self.CdtrSchmeId.Id.PrvtId.Othr->forAll(q | q.Id.matches('[A-Z][A-Z][0-9][0-9][A-Za-z0-9]+'))
-- Checking regular expressions inside a repetitive element (Othr is [1..n])...
self.UltmtDbtr.PstlAdr.Ctry = "IT" implies
self.UltmtDbtr.Id.OrgId.Othr->forAll(o |
o.Id.matches('^[A-Z]{6}[0-9]{2}[A-Z][0-9]{2}[A-Z][0-9]{3}[A-Z]$') implies
o.Issr = "ADE"
)
-- ...and an appropriate query to pinpoint the error message to a specific place inside the repetitive element.
self.UltmtDbtr.Id.OrgId.Othr->select(o |
o.Id.matches('^[A-Z]{6}[0-9]{2}[A-Z][0-9]{2}[A-Z][0-9]{3}[A-Z]$') and
o.Issr <> "ADE"
).Issr
-- Basic latin characters
self.matches("[a-zA-Z0-9/\-?:().,'+ =!\x22 %&\x2A <>;@#${}]*\r\n\x5B \x5D \x5C _\x5E \x60 |\x7E ")
--'Escaping any character
self.MsgId.matches("\u94F6") implies false
--More information about characters:
--http://www.fileformat.info/info/unic...94F6/index.htm ( lists C/C++/Java source codes)
--http://codepoints.net/U+94F6 (general information about a character)
--http://hexed.it/ (shows the encoding of inputted file)
--http://webdesign.about.com/od/locali...odes-ascii.htm
-- Limiting fractional part of number (Causes validation to fail if "let s :" doesn't find anything
-- was .oclAsType(decimal).toString(). Was updated some time ago so casting to integer not needed anymore. if it exists somewhere, will cause building to fail
self.Amt.InstdAmt->size() = 1 implies (
let s : string = self.Amt.InstdAmt.toString()
in
s.contains('.') implies
(s.indexOf('.') = s.size() - 3 or
s.indexOf('.') = s.size() - 2)
)
-- also note:
s.indexOf('.') > (s.size()-4)
-- if currency amt can be ended with .
-- All the Ids (if given) must be same
DirectDebitTransactionInformation9.allInstances()->DrctDbtTx.CdtrSchmeId.Id.PrvtId.Othr.Id->asSet()->size() <= 1
-- Same written differently
self.PmtInf->collect(a | a. PmtTpInf.SeqTp)->asBag()->size() > 1 implies (
self.PmtInf->collect(a | a. PmtTpInf.SeqTp)->asBag()->size() <>
self.PmtInf->collect(a | a. PmtTpInf.SeqTp)->asSet()->size()
)
--No line feeds
self.Ustrd->size() > 0 implies not(self.Ustrd->exists(q | q.matches("(.*[\r|\n|\r\n].*)+")))
--Query
self.Ustrd->select(q | q.matches("(.*[\r|\n|\r\n].*)+"))
--All Ids are unique
self->allInstances()->collect(a | a. PmtInfId)->asBag()->size() =
self->allInstances()->collect(a | a. PmtInfId)->asSet()->size()
--This would give error message to the beginning of every context.
--All the substrings of Ids up until a fixed string are unique
AttachmentDetailsType.allInstances().AttachmentIdentifier->collect(a | a.substring(1, a.indexOf('::attachment')))->asBag()->size() =
AttachmentDetailsType.allInstances().AttachmentIdentifier->collect(a | a.substring(1, a.indexOf('::attachment')))->asSet()->size()
--Unique id's used
self.allInstances()->collect(q | q.PmtId.EndToEndId)->asBag()->size() =
self.allInstances()->collect(q | q.PmtId.EndToEndId)->asSet()->size()
--Checks that InstrId is not larger than the total occurrence amount of InstrId. Checks that the value one exists in InstrId
--I.E. Values of InstrId are from 1 to size(), in whatever order
self.CdtTrfTxInf.PmtId.InstrId->forAll(q |
q.oclAsType(Integer) <= self.CdtTrfTxInf.PmtId.InstrId->size()and
q.isNumeric() ) and
self.CdtTrfTxInf.PmtId.InstrId->exists(q |
q.oclAsType(Integer) = 1
)
--Values of InstrId are from 1 to size(). In that order. Also no duplicates allowed.
(self.CdtTrfTxInf.PmtId.InstrId->asBag()->size() = self.CdtTrfTxInf.PmtId.InstrId->asSet()->size()) and
self.CdtTrfTxInf.PmtId.InstrId->asSequence()->forAll(q |
q.oclAsType(Integer) = self.CdtTrfTxInf.PmtId.InstrId->asSequence()->indexOf(q)
)
--All ids are unique, with escaping of certain values
self.PmtInf.CdtTrfTxInf->collect(q | q.PmtId.EndToEndId)->asBag()->select(e |e <> "NOTPROVIDED")->size() =
self.PmtInf.CdtTrfTxInf->collect(q | q.PmtId.EndToEndId)->asSet()->select(e |e <> "NOTPROVIDED")->size()
--Number of transactions
self.GrpHdr.NbOfTxs.oclAsType(integer) =
PaymentInstructionInformation3.allInstances()->CdtTrfTxInf->size().oclAsType(integer)
--May not contain å, ä, ö
self.MsgId.matches("[a-zA-Z0-9?\-/().,+{}:]+")
--EPC characters (http://wiki.xmldation.com/Support/EPC/Latin_Character_Set)
self.matches("[a-zA-Z0-9/\-?:().,\x27 +]*")
--May not contain only space, line feed, carriage return, tab (has to contain information)
self.Mndt.MndtReqId.matches('^[ \n\r\t]*$') implies false
--Only cyrillic and latin chars and size (http://www.regular-expressions.info/unicode.html)
self.RmtInf.Ustrd->forAll(q | q.size() <= 25 and
q.matches("[\p{InBasic_Latin}\p{InCyrillic}]*") )
--Schema location
schemaLocation = 'urn:iso:std:iso:20022:tech:xsd:pain.001.001.03 pain.001.001.03.xsd'
--IBAN and BIC match example (only in countries where they can be checked)
(self.CdtrAcct.Id.IBAN.matches('NL.*') and self.CdtrAgt.FinInstnId.BIC->size() = 1) implies
CdtrAcct.Id.IBAN.substring(4,8) = CdtrAgt.FinInstnId.BIC.substring(0,4)
(self.DbtrAcct.Id.IBAN.matches('NL.*') and self.DbtrAgt.FinInstnId.BIC->size() = 1) implies
DbtrAcct.Id.IBAN.substring(4,8) = DbtrAgt.FinInstnId.BIC.substring(0,4)
-- Countries in IBAN and BIC have to match
CdtrAcct.Id.IBAN.substring(0,2) = CdtrAgt.FinInstnId.BIC.substring(4,6)
--InstdAmt matches with values in RmtInf
self.RmtInf.Strd->size() >= 2
implies
(
self.Amt.InstdAmt->sum().oclAsType(decimal) = (
self.RmtInf.Strd->collect( q | q.RfrdDocAmt.RmtdAmt )->asBag()->sum().oclAsType(decimal) -
self.RmtInf.Strd->collect( q | q.RfrdDocAmt.CdtNoteAmt )->asBag()->sum().oclAsType(decimal)
)
)
--Limiting multiple instances of element into certain total length
self.AdrLine->collect(a| a.size())->sum() <= 140
--Comparing two ints
self.AIBTrailerRow.TotalNumber = self.AIBDetailRow->collect(q | q->size())->sum()
--Only 140 characters of Strd allowed in finnish, non-aos2, non taxs payments
(
self.CdtrAcct.Id.IBAN->size() = 1 and
self.CdtrAcct.Id.IBAN.matches('[F][I].*') and
self.RmtInf.Strd->size() = 1 and
self.PmtTpInf.CtgyPurp <> "TAXS"
)
implies
self.RmtInf.Strd->forAll(s | s.xmlElementBlockSize() <= 140)
--Finding a specific occurrence and limiting it to a value
self.InitgPty.Id.OrgId.Othr.Issr->asSequence()->at(0) = "CBI"
--Query
self.RmtInf.Strd->select(q | q.xmlElementBlockSize() > 140)
--Summing multiple child elements
let s : AT_PostalAddress6 = self.UltmtDbtr.PstlAdr in
s.hasValue() and s.AdrLine->size() = 0 implies (
s.AdrTp.size() +
s.Dept.size() +
s.SubDept.size() +
s.StrtNm.size() +
s.BldgNb.size() +
s.PstCd.size() +
s.TwnNm.size() +
s.CtrySubDvsn.size() +
s.Ctry.size() < 140
)
--IBAN and bic match for finland example
((self.DbtrAgt.FinInstnId.BIC = "AABAFI22" and self.DbtrAcct.Id.IBAN->size() = 1 )implies
self.DbtrAcct.Id.IBAN.matches('FI\d\d6[0-9]+'))
-- Query for targeting all duplicate EndToEndId's
let allIds : Sequence(String) = self.PmtId.EndToEndId->asSequence() in
allIds->asBag()->iterate(a : String; acc : Set(String) = Set{} |
if(allIds->count(a) > 1) then
acc->including(a.toString())
else
acc
endif)
-- -- or
let allIds : Sequence(Max35Text) = self.PmtId.EndToEndId->asSequence() in
allIds->asBag()->iterate(a : Max35Text; acc : Set(Max35Text) = Set{} |
if(allIds->count(a) > 1) then
acc->including(a)
else
acc
endif)
--Clearing has to match with currency example
(self.CdtrAgt.FinInstnId.CmbndId.ClrSysMmbId.Id.matches('AUBSB.*') or
self.CdtrAgt.FinInstnId.CmbndId.ClrSysMmbId.Id.matches('//AU.*')
implies
self.Amt.InstdAmt.Ccy = "AUD")
--Locating specific occurrence
self.InitgPty.Id.OrgId.Othr->asSequence()->at(0).Issr->size() = 1
implies
self.InitgPty.Id.OrgId.Othr->asSequence()->at(0).Issr = "CBI"
--And same for query
self.InitgPty.Id.OrgId.Othr->asSequence()->at(0).Issr
--2 queries inside one. This returns the locations of both Ctry and PstlAdr
--So both are shown if both exist. Only PstlAdr shown if Ctry doesn't exist
self.FwdgAgt.BrnchId.PstlAdr.Ctry->union(self.FwdgAgt.BrnchId.PstlAdr)
-- Match 13 numbers, 10 numbers and 1 decimal or 10 numbers and 2 decimals.
-- Basically just always check that 10 first are numbers and user OR for the three alternative endings
let s : string = self.TransactionAmount.oclAsType(decimal).toString()
in
s.matches('[0-9]{10}(([0-9]{3})|([0-9]{1}\.[0-9]{1})|(\.[0-9]{2}))')
-- Matches patterns:
-- 0123456789012
-- 01234567890.1
-- 0123456789.12
--Sum length of elements with multiple occurrence (+ element with single occurrence in this case)
self.Dbtr.PstlAdr.AdrLine->collect(q | q.size() + self.Dbtr.PstlAdr.Ctry.size() )->sum() <= 70
-- Modulus 97 check example. Prequisites have to be taken care of with other rules. Done with two rules here when result 0 defaulted to 97
self.CdtrRefInf.Tp.Issr = "BBA" and
self.CdtrRefInf.Ref->size() = 1 and
self.CdtrRefInf.Ref.size() = 12 and
self.CdtrRefInf.Ref.matches('[0-9]+') and
self.CdtrRefInf.Ref.substring(0,10).toInteger() mod 97 = 0
implies (
self.CdtrRefInf.Ref.substring(10,12).toInteger()
=
97 )
self.CdtrRefInf.Tp.Issr = "BBA" and
self.CdtrRefInf.Ref->size() = 1 and
self.CdtrRefInf.Ref.size() = 12 and
self.CdtrRefInf.Ref.matches('[0-9]+') and
self.CdtrRefInf.Ref.substring(0,10).toInteger() mod 97 <> 0
implies (
self.CdtrRefInf.Ref.substring(0,10).toInteger() mod 97
=
self.CdtrRefInf.Ref.substring(10,12).toInteger()
)
--isValidRF()
self.Strd->forAll(s |
s.CdtrRefInf.Ref.matches('RF[A-Za-z0-9]+') implies
s.CdtrRefInf.Ref.isValidRF()
)
--isValidFinnish reference
--Triggered when ref starts with number or when SCOR and no Issr is given. (Issr ISO triggers RF Creditor Reference)
((self.CdtrRefInf.CdtrRef->size() = 1 and self.CdtrRefInf.CdtrRef.matches('\d.*') ) or
(self.CdtrRefInf.CdtrRefTp.Issr->size() = 0 and self.CdtrRefInf.CdtrRefTp.Cd = "SCOR")
)
implies (self.CdtrRefInf.CdtrRef.isValidReference("FI"))
--isValidIBAN()
self.IBAN->size() = 1 implies self.IBAN.isValidIBAN()
--allowedDaysInPast() / future
ReqdExctnDt.allowedDaysInPast(5)
--Date between certain range (+5 and +90(yes, +90))
(self.ReqdColltnDt->size() = 1 and
self.PmtTpInf.LclInstrm.Cd->size() = 1 and
self.PmtTpInf.LclInstrm.Cd = 'CORE'
)
implies
self.ReqdColltnDt.allowedDaysInPast(0) and
not(self.ReqdColltnDt.allowedDaysInFuture(5)) and
self.ReqdColltnDt.allowedDaysInFuture(91)
--Date Comparison
self.ReqdExctnDt.after(self.PoolgAdjstmntDt)
--Date within two weeks of another date
self.PoolgAdjstmntDt->size() >= 1 implies self.PoolgAdjstmntDt.before(self.ReqdExctnDt.plusDays(14)) and self.PoolgAdjstmntDt.after(self.ReqdExctnDt.minusDays(14))
--Date exactly after two weeks of another date
self.PoolgAdjstmntDt->size() >= 1 implies self.PoolgAdjstmntDt.after(self.ReqdExctnDt.plusDays(13)) and self.PoolgAdjstmntDt.before(self.ReqdExctnDt.plusDays(15))
--DateTime in following format Usage rule: Only valid ISO Date Time is allowed: YYYY-MM-DDThh:mm:ss
self.CreDtTm.toString().matches("^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$")
--Alternative way for writing rules, using let, in
self.PmtTpInf.LclInstrm.Cd->size() = 1 implies
self.PmtTpInf.LclInstrm.Cd = 'B2B' or
self.PmtTpInf.LclInstrm.Cd = 'CORE'
- ->
let s : string = self.PmtTpInf.LclInstrm.Cd in
s.hasValue() implies (s = 'CORE' or s = 'B2B')
also
self.CdtrSchmeId.Id.PrvtId.Othr->exists(q | q.Id->size()= 1) and
self.CdtrSchmeId.Id.PrvtId.Othr->exists(q | q.Id.matches('BE.*'))
implies
self.CdtrSchmeId.Id.PrvtId.Othr->forAll(q | q.Id.matches('^..[0-9][0-9].*'))
- ->
-- Two lets:
-- Note that part before implies has to refer to actual paths and not the variables. Variables are apparently filled with something
-- even when element doesn't exist. This might be substring related as hasValue() works as it should normally
let IBAN : string = DbtrAcct.Id.IBAN.substring(0,2)
let BIC : string = DbtrAgt.FinInstnId.BIC.substring(4,6) in
DbtrAcct.Id.IBAN->size() = 1 and DbtrAgt.FinInstnId.BIC->size() = 1 implies
IBAN = BIC
-- let and bag
let s : Bag(string) = self.CdtrSchmeId.Id.PrvtId.Othr.Id in
s->exists(q | q.hasValue()) and s->exists(q | q.matches('BE.*')) implies (
s->exists(q | q.matches('^..[0-9][0-9].*') )
)
--Queries
self.CdtrSchmeId.Id.PrvtId.Othr->select(o | o.SchmeNm->size() = 1
and o.SchmeNm.Prtry->size() = 0)
-- Query targeting the last element if multiple available:
-- can be used e.g. if only one occurrence allowed.
-- Example here for DDv02 schema. Error message targted to last occurrence of RgltryRptg if there are more than one present.
context: DirectDebitTransactionInformation9
rule: self.RgltryRptg->size() < 2
query: self.RgltryRptg->asSequence()->at(self.RgltryRptg->size() - 1)
---- NOTE regarding rules triggering from PmtInf and in Tx lvls, affecting Tx. Following does not work properly:
self.PmtMtd <> 'CHK'
and
self.CdtTrfTxInf->exists(c | c.Purp.Cd = "TAXS" and c.RmtInf->size() = 1)
implies
self.CdtTrfTxInf->forAll(c | c.RmtInf.Ustrd->size() >= 1)
--(singlular occurrence in exists triggering all occurrences with forAll)
-- And neither does the following:
self.PmtMtd <> 'CHK'
and
self.CdtTrfTxInf->forAll(c | c.Purp.Cd = "TAXS" and c.RmtInf->size() = 1
implies
c.RmtInf.Ustrd->size() >= 1)
-- This does (checks and implies inside forAll)
self.CdtTrfTxInf->forAll(c | self.PmtMtd <> 'CHK' and c.Purp.Cd = "TAXS" and c.RmtInf->size() = 1
implies
c.RmtInf.Ustrd->size() >= 1)
-- No accented chars allowed
self.matches('[^àèìòùÀÈÌÒÙáéíóúýÁÉÍÓÚÝâêîôûÂÊÎÔÛãñõÃÑÕäëïöüÿÄËÏÖÜŸçÇßØøÅåÆæœßẞ]*')
-- implies
(
self.InvoiceTotalVatIncludedAmount->size() > 0 and
self.InvoiceTotalVatExcludedAmount->size() > 0 and
self.InvoiceTotalVatAmount->size() > 0
)
implies
(
self.InvoiceTotalVatIncludedAmount.toReal() =
self.InvoiceTotalVatExcludedAmount.toReal() + self.InvoiceTotalVatAmount.toReal()
-- Invalid characters regex example
self.matches('[^áäçéíóöúüýæøåÁÄÇÐÉÍÓÖÚÜÝÆØÅß%&=@§]*')
-- also ( (?s). makes dot accept spaces)
self.matches('(?s).*[áäçéíóöúüýæøåÁÄÇÐÉÍÓÖÚÜÝÆØÅß%&=@§]+.*') implies false
)
--Counts the data within complexType
self.CdtrRefInf.xmlDataSize() <= 15
--SEPA countries 3.7.2015. Canary islands (ES) and Croatia (HR) were added
self->size() = 1 implies (
self.matches('FI.+') or
self.matches('AT.+') or
self.matches('PT.+') or
self.matches('BE.+') or
self.matches('BG.+') or
self.matches('ES.+') or
self.matches('HR.+') or
self.matches('CY.+') or
self.matches('CZ.+') or
self.matches('DK.+') or
self.matches('EE.+') or
self.matches('FR.+') or
self.matches('DE.+') or
self.matches('GI.+') or
self.matches('GR.+') or
self.matches('HU.+') or
self.matches('IS.+') or
self.matches('IE.+') or
self.matches('IT.+') or
self.matches('LV.+') or
self.matches('LI.+') or
self.matches('LT.+') or
self.matches('LU.+') or
self.matches('MT.+') or
self.matches('MC.+') or
self.matches('NL.+') or
self.matches('NO.+') or
self.matches('PL.+') or
self.matches('RO.+') or
self.matches('SM.+') or
self.matches('SK.+') or
self.matches('SI.+') or
self.matches('ES.+') or
self.matches('SE.+') or
self.matches('CH.+') or
self.matches('GB.+')
)
--SOAP <-> Finvoice crosscheck rule:
-- Context: Finvoice
-- For at least one instance of Envelope/Header/MessageHeader/From/To[Role="Receiver"]/PartyId there must be a Finvoice/MessageTransmissionDetails/MessageReceiverDetails/ToIdentifier with an identical value, in a corresponding position (e.g. Envelope[1] <-> Finvoice[1], etc.).
-- Same rule as above with MessageHeader (Envelope/Header/MessageHeader) as context:
let iWeightedTotals : interger = (
(sNb.substring(0, 1) * 3) +
(sNb.substring(1, 2) * 7) +
(sNb.substring(2, 3) * 1) +
(sNb.substring(3, 4) * 3) +
(sNb.substring(4, 5) * 7) +
(sNb.substring(5, 6) * 1) +
(sNb.substring(6, 7) * 3) +
(sNb.substring(7, 8) * 7) )
let iCalculatedCheckDigit : integer = iWeightedTotals mod 10 in
let iCheckDigit : integer = self.Field3.toInteger() in
let bIsNbNumeric : boolean = self.Field3.isNumeric() in
let bIsCheckDigitNumeric : boolean = self.Field4.isNumeric() in
let bfieldsAreNumerc : boolean = bIsNbNumeric and bIsCheckDigitNumeric in
bfieldsAreNumerc implies iCalculatedCheckDigit = iCheckDigit