How can I convert a 15 char Id value into an 18 char Id value?

Explanation of the algorithm: This is based on the algorithm given here. The example below is using a made up salesforce 15 char Id 001A000010khO8J

  1. Separate the 15 char Id into 3 groups of 5 chars. You now have 3 strings (the triplet variable below): 001A0, 00010 and khO8J
  2. Reverse each string. The three strings are now 0A100, 01000 and J8Ohk
  3. In each string, convert all Uppercase chars to 1, all other chars to 0. The three strings are now 01000, 00000 and 10100.
  4. Look up the corresponding char in the BinaryIdLookup based. This gives us a suffix of IAU.
  5. The 3 chars generated (in order) are appended to the 15 char Id value, giving you an 18 char Id value of 001A000010khO8JIAU.

I have created an implementation of this in C# and have tested this on a number of real Salesforce Ids and it seems to do the job. Code is in this gist or below:

static string Convert15CharTo18CharId(string id)
{
    if (string.IsNullOrEmpty(id)) throw new ArgumentNullException("id");
    if (id.Length == 18) return id;
    if (id.Length != 15) throw 
       new ArgumentException("Illegal argument length. 15 char string expected.", "id");

    var triplet = new List<string> { id.Substring(0, 5), 
                                     id.Substring(5, 5), 
                                     id.Substring(10, 5) };
    var str = new StringBuilder(5);
    var suffix = string.Empty;
    foreach (var value in triplet)
    {
        str.Clear();
        var reverse = value.Reverse().ToList();
        reverse.ForEach(c => str.Append(Char.IsUpper(c) ? "1" : "0"));
        suffix += BinaryIdLookup[str.ToString()];
    }
    return id + suffix;
}

static readonly Dictionary<string, char> BinaryIdLookup = new Dictionary<string, char>
{
    {"00000", 'A'}, {"00001", 'B'}, {"00010", 'C'}, {"00011", 'D'}, {"00100", 'E'},
    {"00101", 'F'}, {"00110", 'G'}, {"00111", 'H'}, {"01000", 'I'}, {"01001", 'J'},
    {"01010", 'K'}, {"01011", 'L'}, {"01100", 'M'}, {"01101", 'N'}, {"01110", 'O'}, 
    {"01111", 'P'}, {"10000", 'Q'}, {"10001", 'R'}, {"10010", 'S'}, {"10011", 'T'}, 
    {"10100", 'U'}, {"10101", 'V'}, {"10110", 'W'}, {"10111", 'X'}, {"11000", 'Y'}, 
    {"11001", 'Z'}, {"11010", '0'}, {"11011", '1'}, {"11100", '2'}, {"11101", '3'}, 
    {"11110", '4'}, {"11111", '5'}
};

Java version:

public class SalesforceIDConverter
{
    public static String convertID(String id)
    {
        if(id.length() == 18) return id;

        String suffix = "";
        for(int i=0;i<3;i++){

            Integer flags = 0;

            for(int j=0;j<5;j++){
                String c = id.substring(i*5+j,i*5+j+1);

                if(c.compareTo("A")  >= 0 && c.compareTo("Z") <= 0){

                    flags += 1 << j;
                }
            }

            if (flags <= 25) {

                suffix += "ABCDEFGHIJKLMNOPQRSTUVWXYZ".substring(flags,flags+1);

            }else suffix += "012345".substring(flags-26,flags-26+1);
        }

        return id+suffix;
    }

    public static void main(String[] args)
    {
        String id =  "001M0000009odAH";
        String convertedID = convertID(id);
        System.out.println("id: " + id + "; converts to: " + convertedID);
    }
}

Python version

import string
import sys

bin_lookup = {
    '00000':'A',
    '00001':'B',
    '00010':'C',
    '00011':'D',
    '00100':'E',
    '00101':'F',
    '00110':'G',
    '00111':'H',
    '01000':'I', 
    '01001':'J', 
    '01010':'K', 
    '01011':'L', 
    '01100':'M', 
    '01101':'N', 
    '01110':'O', 
    '01111':'P', 
    '10000':'Q',
    '10001':'R',
    '10010':'S',
    '10011':'T',
    '10100':'U',
    '10101':'V',
    '10110':'W',
    '10111':'X',
    '11000':'Y',
    '11001':'Z',
    '11010':'0',
    '11011':'1',
    '11100':'2',
    '11101':'3',
    '11110':'4',
    '11111':'5'
}

def sf_replace(incoming_id_char):

    if incoming_id_char in string.ascii_uppercase:
        return '1'
    else:
        return '0'


def expand_sf_id(incoming_sf_id):

    if len(incoming_sf_id) != 15:
        raise ValueError('id string must be exactly 15 characters long')

    # split into list of 3 5-character chunks
    id_chunks = map("".join, zip(*[iter(incoming_sf_id)]*5))

    suffix = ''

    for id_chunk in id_chunks:

        # replace all capital letters with 1, non cap letters with 0
        lookup_components = [sf_replace(id_char) for id_char in id_chunk]

        # make it string and reverse it
        lookup_chunk = "".join(lookup_components)[::-1]

        # get the letter from the lookup table based on the reversed string
        bin_replacement = bin_lookup[lookup_chunk]
        suffix += bin_replacement

    #add the suffix to the origional id
    expanded_sf_id = incoming_sf_id + suffix

    return expanded_sf_id 


if __name__ == '__main__':
    print expand_sf_id(sys.argv[1])