1

We have the need to insert price ranges (between Quantidade Quantidade_Cx_Minima__c and_Cx_Maxima__c) dynamically (in the object Escalao_de_Promocao__c), for specific products in specific stores, so that they handle promotions in stores. The quantities of a product relates to the discount on unit price. The type of discount accepted are configured in a related product custom object.

The manager inserts ranges integers like 0-9, unit price or discount percentage (there is a validating rule demanding one of the fields filled) = 20€ or -1%;

so 10 to 19 = 18€ or -3%; etc.

table of registered Escalao_de_Promocao__c

These ranges can be sequential, or if there is a gap, or a range is deleted, it can be filled later. Like: with these ranges: 10-19, 30-39, 40-49 these inserts are valid: 0-9, 20-25, 50-59

and these should be invalid: 0-20, 20-25, 20-60

the validation is now made with a trigger a few validation rules: the trigger:

public with sharing class tl_Escalao_de_Promocao {
private th_Escalao_de_Promocao handler;
public tl_Escalao_de_Promocao(th_Escalao_de_Promocao handler) {    
    this.handler = handler;        
}

public void ValidateEscaloesPromocoes(List<Escalao_de_Promocao__c> l_EscalaoPromocao) {        
    List<String> sIDPromocao = new List<String>();
    List<String> sIDProduto = new List<String>();
    
    for(Escalao_de_Promocao__c ep: l_EscalaoPromocao){
        sIDPromocao.add(ep.Promocao__c);
        sIDProduto.add(ep.Produto_da_Promocao__c);
    }
    if(sIDPromocao.size()>0 || sIDProduto.size()>0){
        
        Map<Decimal, Decimal> Map_Number = new Map<Decimal, Decimal>();
        List<Escalao_de_Promocao__c> escaloes = [
            SELECT Id, Quantidade_Cx_Minima__c, Quantidade_Cx_Maxima__c 
            FROM Escalao_de_Promocao__c 
            WHERE Promocao__c IN: sIDPromocao and Produto_da_Promocao__c IN: sIDProduto ];

        for(Escalao_de_Promocao__c e: escaloes){

            for(Decimal i= e.Quantidade_Cx_Minima__c; i <= e.Quantidade_Cx_Maxima__c; i++ ){
                Map_Number.put(i, i);
            }
        }

        system.debug('Map_Number: ' + Map_Number);
        for(Decimal valores: Map_Number.keySet()){
            for(Escalao_de_Promocao__c esc : l_EscalaoPromocao ){
                if(Map_Number.keySet().contains(esc.Quantidade_Cx_Minima__c) || 
                Map_Number.keySet().contains(esc.Quantidade_Cx_Maxima__c)){
                    if(!Test.isRunningTest()) esc.addError('ERRO'); 
                }
            }
        }
    }       
}

The process here is:

  • mapping all integer values between minumim and maximum values, for recorded Escalao_de_Promocao__c objects
  • if the value is already there, cant be saved. so for ranges 0-4, 10-14, 30-39, befores the insert, we have
    Map_Number is: {0=false, 1=false, 2=false, 3=false, 4=true, 6=true, 10=false, 11=false, 12=false, 13=false, ...}

so far, gives an interface error with inserting:

  • repeated range
  • overlapping range

with validation rules we validate maximum bellow minimum.

But we cant validate if a range starts and ends in empty slots, while overlaping a filled range. So inserting this should also be invalid: 8-29. And here is the remaining issue.

Does anyone have a better solution? Thanks

edit: my implementation

public void ValidateEscaloesPromocoes(List<Escalao_de_Promocao__c> l_EscalaoPromocao) {        
    List<String> sIDPromocao = new List<String>();
    List<String> sIDProduto = new List<String>();
    Set<Decimal> allocatedRanges = new Set<Decimal>();
    
    for(Escalao_de_Promocao__c ep: l_EscalaoPromocao){
        sIDPromocao.add(ep.Promocao__c);
        sIDProduto.add(ep.Produto_da_Promocao__c);
    }
    if(sIDPromocao.size()>0 || sIDProduto.size()>0){
        
        Set<Decimal> currentRange = new Set<Decimal>();

        List<Escalao_de_Promocao__c> escaloes = [SELECT Id, Quantidade_Cx_Minima__c, Quantidade_Cx_Maxima__c FROM Escalao_de_Promocao__c WHERE Promocao__c IN: sIDPromocao and Produto_da_Promocao__c IN: sIDProduto ];

        System.debug('escaloes: ' + escaloes);

        for(Escalao_de_Promocao__c rec : escaloes){
            for(Decimal i = rec.Quantidade_Cx_Minima__c; i <= rec.Quantidade_Cx_Maxima__c; i++){  
                currentRange.add(i);
            }
        }
        System.debug('allocatedRanges: ' + allocatedRanges);

        if(!currentRange.clone().removeAll(allocatedRanges)){
            allocatedRanges.addAll(currentRange);
            System.debug('currentRange s overlap: ' + currentRange);
        } else {
            System.debug('allocatedRanges overlap: ' + allocatedRanges);
            if(allocatedRanges.containsAll(currentRange)){
                System.debug('currentRange contains: ' + currentRange);
            } else {
                System.debug('currentRange not contains: ' + currentRange);
            }
        } 

        System.debug('allocatedRanges: ' + allocatedRanges); 
    }       
}

1 Answer 1

0

I think that what you have is fairly close.

While there are some datasets that have nice properties that can help us design an algorithm (IP Address allocation maps very nicely to a binary tree, for example), I don't think that this problem really gives us much to work with.

About the only suggestion that I can come up with is I don't think using a Map is appropriate here. Instead, I'd use a simple Set<Decimal>.

The idea is to turn each range into a Set<Decimal> (i.e. Minimum = 9, Maximum = 16 becomes Set<Decimal>{9, 10, 11, 12, 13, 14, 15, 16}), and then use a combination of removeAll(), retainAll(), and containsAll() to check for overlap.

// I'm just assuming that there's no issue with combining the existing and new records
// That assumption means that existing records are re-subjected to the overlap check
// We need to generate ranges for them (the existing records) anyway, and I don't think
//   that the Set class methods consume too much cpu time
Set<Decimal> allocatedRanges = new Set<Decimal>();
for(Record__c rec :allRecords){
    Set<Decimal> currentRange = new Set<Decimal>();
    for(Decimal i = rec.min; i <= rec.max; i++){    
        currentRange.add(i);
    }

    // removeAll returns a boolean
    // false = original set was not changed
    // true = original set did undergo a change
    // The extra clone() call in there helps ensure that currentRange is not modified
    //   (so that we can use it to help determine what type of overlap we have)
    if(!currentRange.clone().removeAll(allocatedRanges)){
        // In here, we know that there is no overlap between currentRange
        //   and allocatedRanges
        // Add current to allocated so we can check the next record
        allocatedRanges.addAll(currentRange);
    }else{
        // Inside this else block, we know that there was _some_ overlap
        // Determining if that's due to partial overlap, a repeated range,
        //   or a sub-range requires extra work
        if(allocatedRanges.containsAll(current)){
            // In here, we know that we either repeated a range, or are a
            //   sub-range
            // Figuring out which it is, though, may not be possible without
            //   additional data
        }else{
            // In here, we know that there is some non-complete overlap
            // i.e. not all of the range has already been allocated
        }
    }
}

A pseudo-code description of the above (which may be more readable) is

declare a primary set for things that have already been allocated
for each Escalao_de_Promocao__c record{
    declare a second set to hold the range for _this_ particular record
 
    iterate from record min to record max (to build our range){
        add this number to our secondary set
    }

    does our secondary set contain any numbers from the primary set?{
        no, it does not, there is no overlap
        add our secondary set into the primary set
        continue on to checking the next record
    }{
       yes, it does, so there is overlap
       error out
    }
}

It's important that you keep checking for overlap and adding to the allocated set for each individual record.

There may be a way to do this without needing to expand a range into a Set, but unless you're running into the CPU or Heap governor limits I don't think it's worth trying to come up with something more clever than this.

5
  • thanks for the answer: Seems the way to go. So, if i understand correctly, a valid insert should enter in: System.debug('currentRange s overlap: ' + currentRange); And an invalid on in the else after. So, with ranges 0 - 2, 10 - 12, i insert 9 - 20, an invalid insert. And i get: allocatedRanges: {} currentRange s overlap: {0, 1, 2, 3, 4, 5, 6, 10, 11, 12} allocatedRanges: {0, 1, 2, 3, 4, 5, 6, 10, 11, 12} It never goes to the invalid one! Commented Jul 23, 2020 at 14:30
  • i edited my question to include an implementation of your code, but it's not visible yet. and yes, if all the inputs in the trigger are valid, mixing them with existing records is fine Commented Jul 23, 2020 at 14:38
  • aren't you suposed to add something to allocatedRanges, so i can be evaluated in if(!currentRange.clone().removeAll(allocatedRanges)){} ? Commented Jul 23, 2020 at 14:50
  • @LuísAguiar It looks like your implementation is structuring the check differently. You're adding everything to currentRange before checking whether or not there's overlap whereas I'm building a new range for each record encountered, and checking for overlap on each record. Put another way, I have my if(!currentRange.clone().removeAll(allocatedRanges)) inside the outer loop, but you have that check outside of all loops.
    – Derek F
    Commented Jul 23, 2020 at 16:21
  • and allRecords are the existing records, before inserting the trigger. So where are you getting the trigger information? Commented Jul 24, 2020 at 10:31

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .