Page last modified 10:37, 1 Nov 2016 by Jussi

Example OCL rules

    Table of contents
    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)

    --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)

     


    -- 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.). 

    let SFinvoice : Sequence(Finvoice) = 
    Finvoice.allInstances()->asSequence() in
     
    let SMessageHeader : Sequence(MessageHeader) =
    MessageHeader.allInstances()->asSequence() in
     
    let finvoiceId : string = 
    self.MessageTransmissionDetails.MessageReceiverDetails.ToIdentifier in
     
    let BPartyId : Bag(PartyId) = 
    SMessageHeader
    ->at(SFinvoice->indexOf(self)).To
    ->select(t | t.Role = "Receiver")
    ->collect(t | t.PartyId) in
     
    BPartyId->size() > 0 implies
    BPartyId->exists(soapId | soapId = finvoiceId)

     

    -- Same rule as above with MessageHeader (Envelope/Header/MessageHeader) as context:

    let SFinvoice : Sequence(Finvoice) = 
    Finvoice.allInstances()->asSequence() in
     
    let SMessageHeader : Sequence(MessageHeader) =
    MessageHeader.allInstances()->asSequence() in
     
    let BPartyId : Bag(PartyId) =
    self.To
    ->select(f | f.Role = "Receiver")
    ->collect(f | f.PartyId) in
     
    BPartyId->size() > 0
    implies
    BPartyId->exists(id |
        SFinvoice->exists(fv | 
            id = fv.MessageTransmissionDetails.MessageReceiverDetails.ToIdentifier and
            SMessageHeader->indexOf(self) = SFinvoice->indexOf(fv)
        )
    )
     
    -- Query that selects elements that have LanguageCode attributes with values occurring more than once:
    let sift : Bag(TextLanguageOptional) = self.SellerInstructionFreeText in
    sift->select(s1 | 
    sift->select(s2 | 
    s2.LanguageCode = s1.LanguageCode and sift->asSequence()->indexOf(s2) <> sift->asSequence()->indexOf(s1)
    )->notEmpty()
    )
    Menu