Thursday, January 24, 2013

BUG REPORT: Map's equals and hashCode methods.

Reading through Apex Developer's Guide (winter 13).  There is a new section for Winter 13 that talks about how you can compare if two custom Apex classes are equal or not.  The bug is not directly related to the equals and hashCode methods, but the bug is introduced here because from Winter 13, you are allowed to use non-primitive values as keys in maps.

You can find the documentation at this page:
http://www.salesforce.com/us/developer/docs/apexcode/Content/langCon_apex_collections_maps_keys_userdefined.htm

The following is the definition of PairNumbers class.

public class PairNumbers 
{
  Integer x,y;
  public PairNumbers(Integer a, Integer b) 
  {
    x=a;
    y=b;
  }

  public Boolean equals(Object obj) 
  {
    if (obj instanceof PairNumbers) 
    {
      PairNumbers p = (PairNumbers)obj;
      return ((x==p.x) && (y==p.y));
    }
    return false;
  }

  public Integer hashCode() 
  {
    return (31 * x) ^ y;
  }
}

The way it is implemented, if you create several instances of the class and add them to a Map collection, duplicate keys should be counted as one.    For example, if you have lines of code like this:

Map<PairNumbers, String> m = new Map<PairNumbers, String>();
PairNumbers p1 = new PairNumbers(1,2);
PairNumbers p2 = new PairNumbers(3,4);
PairNumbers p3 = new PairNumbers(1,2);
m.put(p1, 'first');
m.put(p2, 'second');
m.put(p3, 'third');


You should now have two elements in the collection m, that is because p3 have the same key as p1.  As a result, p3 replaces p1 in the collection m.

However, I noticed that, if I append the code with this line:
System.Debug(m.size());

and I run this code in Developer Console, the debug log will show 3 as the number of elements in the collection.

Strangely, when I run this on Eclipse, the debug log from there shows 2, which is the expected result.

This is a new feature introduced in Winter 13, and let's hope this gets fixed soon!


Tuesday, January 15, 2013

How to show data from Map collection in sorted order inside apex:repeat

I was going through Salesforce's Visualforce Developer's Guide and Apex Developer's Guide the last few weeks over the Christmas holiday.  In the Apex guide, for Map collection, the book warned that the "order of objects returned by maps may change without warning", and therefore we are asked to "not reply on the order in which map results are returned".

Then in the Visualforce guide, there is a section on "Referencing Apex Maps and Lists" section in the "Dynamic Visualforce Bindings" chapter.  It talked about how we can use data from map in combination with the <apex:repeat> to display data in the map.

What good is it if it cannot display the data properly, especially if the data set is large?

I'm going to use the example in the Visualforce guide as a demonstration.  It can be found on page 158 (if you are using Winter 13 of the Visualforce guide).  I am also going to add just a little more records so it's easier to see how data is sorted.

The following is the code for the class:

public class p158_a_Blog_Example
{
    public Map<String, String> directorMap {get; set;}
    
    public p158_a_Blog_Example()
    {
        // set up the data in this constructor
        directorMap = new Map<String, String>
                {
                    'Kieslowski' => 'Poland',
                    'Gondry' => 'France',
                    'del Toro' => 'Mexico',
                    'Lee' => 'Taiwan',
                    'Cameron' => 'Canada'
                };
        
    }
}

This is the Visualforce page that makes use of this controller.  I modified it a little so it displays the data in a table.
<apex:page controller="p158_a_Blog_Example">
    <apex:pageBlock title="Directors List">
        <apex:pageBlockTable value="{!directorMap}" var="dirKey" style='width:50%'>
            <apex:column value="{!dirKey}" />
            <apex:column value="{!directorMap[dirKey]}" />
        </apex:pageBlockTable>
    </apex:pageBlock>
</apex:page>

Using different DE orgs, I seem to get the results in different order, so the order of the data is indeed inconsistent.  This is how the data came out this morning:


I would like the data to show Cameron, del Toro, Gondry, Kieslowski and Lee, in this order.

I need to modify the code, but the first lesson of all, if you need to preserve the sorting order, use LIST in your <apex:repeat> tag, do NOT use the map variable.

Now let's go back to the class and turn the Map into a List, so we can display the data sorted.  It actually is very easy.  All you need to do is to add a new List variable, set it and then sort it (via the sort() method of list collection).

The new class is modified as this:
public class p158_a_Blog_Example
{
    public Map<String, String> directorMap {get; set;}
    // new variable to hold the list of directors.
    public List<String> directorList {get; set;}
    
    public p158_a_Blog_Example()
    {
        // set up the data in this constructor
        directorMap = new Map<String, String>
                {
                    'Kieslowski' => 'Poland',
                    'Gondry' => 'France',
                    'del Toro' => 'Mexico',
                    'Lee' => 'Taiwan',
                    'Cameron' => 'Canada'
                };
        // Set the new List variable to be the set of directors using the addAll() method.  After 
        // that simply all the sort function to sort the directors' names.                
        directorList = new List<String>();
        directorList.addAll(directorMap.keySet());
        directorList.sort();                
        
    }
}

The Visualforce also now needs to be modified so it is not using the map variable as the data set, but the list variable instead (which now contains a list of directors, sorted).  NOTE:  you still need to use the Map variable to access the country from which the director came from.  The bolded, underlined text is the ONLY change you need to make to the Visualforce page:
<apex:page controller="p158_a_Blog_Example">
    <apex:pageBlock title="Directors List">
        <apex:pageBlockTable value="{!directorList}" var="dirKey" style='width:50%'>
            <apex:column value="{!dirKey}" />
            <apex:column value="{!directorMap[dirKey]}" />
        </apex:pageBlockTable>
    </apex:pageBlock>
</apex:page>

Well...  still something is not quite right.  


The list is now almost all sorted, except the Mexican director is displayed at the end.  Turns out that when you call the sort() method on the List, it sorts the data based on the ascii code.  The lower case 'd' (ascii code = 100) comes after 'L' (ascii code = 76).  That is, the sort is case-sensitive.

In order to make the sort case-insensitive, what I found the easiest is to add two new variables.

A new Map variable (directorName) is created that maps between the director's name in upper case and the name intact.  A new List variable (directorListIgnoreCase) is created to hold the director's name in upper case for sorting purposes.  This List variable is used to determine the order of the directors.  However the data stored in this variable is all in upper case.  That's why you need the new Map variable directorName to find the director's name in its original form.  Then you can use the original Map variable directorMap to get the country information.

This is the final class.
public class p158_a_Blog_Example
{
    public Map<String, String> directorMap {get; set;}

    // new variable to map the directors from all uppercase to the original name.
    public Map<String, String> directorName {get; set;}
    public List<String> directorListIgnoreCase {get; set;}
    
    public p158_a_Blog_Example()
    {
        // set up the data in this constructor
        directorMap = new Map<String, String>
                {
                    'Kieslowski' => 'Poland',
                    'Gondry' => 'France',
                    'del Toro' => 'Mexico',
                    'Lee' => 'Taiwan',
                    'Cameron' => 'Canada'
                };
        
        directorName = new Map<String, String>();
        directorListIgnoreCase = new List<String>();
        for (String s : directorMap.keySet())
        {
            directorName.put(s.toUpperCase(), s);
            directorListIgnoreCase.add(s.toUpperCase());
        } 
        directorListIgnoreCase.sort();              
        
    }
}

The Visualforce now also is updated to reflect the changes:
<apex:page controller="p158_a_Blog_Example">
    <apex:pageBlock title="Directors List">
        <apex:pageBlockTable value="{!directorListIgnoreCase}" var="dirKey" style='width:50%'>
            <apex:column value="{!directorName[dirKey]}" />
            <apex:column value="{!directorMap[directorName[dirKey]]}" />
        </apex:pageBlockTable>
    </apex:pageBlock>
</apex:page>

Notice the repeating variable is the new List directorListIgnoreCase.  Do remember this is the director's name in upper case, so you don't want to display that.  To display the original director's name, you'll see to reference the new Map variable directorName, passing dirKey as the key.  That's why the director's name is directorName[dirKey].  Finally, to get the country information, we will need to reference the original Map variable diretorMap, passing directorName[dirKey] as the key.

The resulting page now looks exactly what I'm hoping for:


I felt like I had to jump through a lot of hoops to get to the final desired result.  If I have missed some steps, please let me know!