ocl

Finding nested duplicates in OCL


I am facing a challenge when trying to check for duplicates in OCL.

Here is a simplification of the class diagram:

                                       +-----------+
                                       |ChapterName|
                                       +-----------+
                                           ^ 0..* chapterNames
                                           |
                                           ^
                                           V
                 +-------+    books 0..* +----+
                 |Catalog|<>------------>|Book|
                 +-------+               +----+
      catalogs 0..* ^                       ^ 0..* books
                    |                       | 
                  +----+  customers 0..* +--------+
                  |Shop|<>-------------->|Customer|
                  +----+                 +--------+

The attributes for each class are declared as follows:

Problem: What I want to check is if a customer has any books with duplicate chapter names, that also belong to a specific category in catalog.

I haven't managed to wrap my head around the logic. What I have so far is:

context Shop
self.customers.books->select(cubks | cubks =
  self.catalogs.books->select(cabks | cabks = cubks)->first())

...Which should find the books from the catalog which a customer has.

Question: How can I add further constraints to solve the problem above?

Also. I am using Eclipse, EMF, and the OCL console from within Eclipse.


Solution

  • context Shop::checkForDuplicates(catalog:Catalog)
    post: result = 
         self.customer.books->flatten()->select(book|
           catalog.books->contains(book)
         )->forEach(book|
           chapterNames->asSet()->size()=chapterNames->size()
         )
    

    customer is a Set; books is either a Bag or a Set (depending on whether duplicate books are allowed, I'll assume it is a Bag, though it doesn't matter). Then customer.books is a bag of bags of books (one bag per each customer) and customer.books->flatten() is a bag of all the books owned by customers.

    catalog.books is either a Bag or a Set (doesn't matter). The select operation returns only those books that are contained in the given catalog (and belongs to some Customer, since we are selecting from the bag constructed before).

    book.chapterNames is a Sequence (I assume that the association is ordered) with the name of chapters in that book. forAll returns true iff for every element in the collection (i.e. for every book in the given catalog, which is owned by a customer), the body evaluates as true.

    The trick now is relying on the operation Sequence::asSet(), which returns all the elements from the sequence with duplicates removed. Then the size of the bag is equal to the size of the set iff no element was removed (i.e. if every element was unique).