14

I am currently facing an issue where an external system passes back an 18-digit Id that our platform has previously sent to them. However in the process the capitalization of the Id has been removed - read everything has become uppercase. I would imagine somebody has faced this problem before, but apparantly this is not the case.

So in short; is somebody aware of any existing (apex-) function that converts uppercased 18-digit Ids to a valid Salesforce Id?

Please beware; while a lot of answers exist for converting 15 to 18 digits, which is trivial, this is a different problem.

Koen Faro
  • 773
  • 4
  • 14

6 Answers6

14

I ported Daniel Ballinger's answer here to Apex:

static String CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ012345';

static List<Boolean> getBitPattern(String c)
{
    Integer index = CHARS.indexOf(c.toUpperCase());
    List<Boolean> result = new List<Boolean>();
    for (Integer bitNumber = 0; bitNumber < 5; bitNumber++)
        result.add((index & (1 << bitNumber)) != 0);
    return result;
}
static String repairCasing(String input)
{
    if (String.isBlank(input) || input.length() != 18) return input;

    List<Boolean> toUpper = new List<Boolean>();
    toUpper.addAll(getBitPattern(String.valueOf(input.substring(15, 16))));
    toUpper.addAll(getBitPattern(String.valueOf(input.substring(16, 17))));
    toUpper.addAll(getBitPattern(String.valueOf(input.substring(17, 18))));

    String output = '';
    for (Integer i = 0; i < 15; i++)
    {
        String c = String.valueOf(input.substring(i, i+1));
        output += toUpper[i] ? c.toUpperCase() : c.toLowerCase();
    }
    output += input.substring(15, 18).toUpperCase();
    return output;
}

I tested it and it worked:

Id value1 = Id.valueOf('00129000007Kbn7AAC');
Id value2 = Id.valueOf('00129000007KBN7AAC');

system.assertEquals(value1, repairCasing(value2));
Robs
  • 9,336
  • 20
  • 106
  • 215
Adrian Larson
  • 149,971
  • 38
  • 239
  • 420
  • can you explain how toUpper[i] ? c.toUpperCase() : c.toLowerCase() works? I'm trying to recreate this in python now and I'm not familiar with that syntax – Jwok Nov 30 '18 at 01:16
  • @Jwok That seems like a great question to post. Specifically, you would likely get good responses to a post asking how to convert this algorithm to Python. Especially if you can post what you have already done to try to convert, and where specifically you are stuck. Please use the Ask Question link to pose it to the broader community. – Adrian Larson Nov 30 '18 at 21:33
  • 1
    FYI it's ternary syntax. In Python you'd use c.upper() if toUpper[i] else c.lower(). – Adrian Larson Nov 30 '18 at 21:34
  • 1
    got it figured out and posted it to this thread. I also created a new post on stack overflow here for a signal boost – Jwok Dec 01 '18 at 18:34
  • 2
    There's a typo on line 16 - should be input.substring(15, 16). This won't matter for most Ids (as most start with numbers) but any Ids that start with letters will have all those letters made lowercase. – YodaDaCoda May 31 '19 at 01:42
  • 1
    Interesting. It did work before. I'll have to test it out before I update the post content. – Adrian Larson May 31 '19 at 01:43
  • The code did not work for me. I implemented @YodaDaCoda suggestion and it worked, so I've updated the post. – Robs Jul 02 '19 at 12:06
10

Parallel to Adrian's response I also cooked up my own version for reference;

public static Id correctCapitalizedId(String input) {
    String keyString = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ012345', result = '';
    Integer i = 0, s = 15;

    if (input.length() != 18) {
        return null;
    }

    for (String seq : input.split('(?<=\\G.{5})')) {
        Integer chk = keyString.indexOf(input.mid(s++,1));
        i = 0;
        for (String chr : seq.split('')) {
            result += seq.length() != 5  ? '' : (chk & (i == 0 ? ++i : (i*=2))) == 0 ? chr.toLowerCase() : chr;
        }
    }
    return Id.valueOf(result);
}
Koen Faro
  • 773
  • 4
  • 14
  • 1
    Elegant. Way to take the cleanup a step further! – Adrian Larson May 24 '16 at 20:37
  • 2
    Had to edit my code slightly - it was not working correctly in the sense that the last 3 characters were always lowercased - this causes Id.valueOf to render an Id - but subsequently actually using this Id while trying to populate/commit a lookup-field will still render a nasty error.... – Koen Faro May 24 '16 at 21:46
  • This fails if any part of the 3 character checksum is lowercase. Adding input = input.toUpperCase() at the top of the function fixes the problem. – Jason Clark Jun 16 '23 at 18:44
3

Old thread, but new apex specific solution: As of version 54.0 you can simple use Id.valueOf(str, restoreCasing), see here for more information: https://developer.salesforce.com/docs/atlas.en-us.236.0.apexref.meta/apexref/apex_methods_system_id.htm#apex_System_id_valueOf_2

loeachim
  • 463
  • 1
  • 4
  • 9
1

Given that the 18 character form of an ID is designed to be a unique identifier irrespective of the casing, I'm not sure why you would ever need to convert to a 15 digit ID.

Why not stay with the 18 character values? If you did for example need to use them as string map keys just normalise their case by using toLowerCase() on them. They remain valid ID values and can be converted to the type Id using Id.valueOf.

Keith C
  • 135,775
  • 26
  • 201
  • 437
  • You cannot query on it or view the detail page via url if the casing is incorrect. Try it in the dev console. Worth taking a look at the post I flagged as duplicate. – Adrian Larson May 24 '16 at 20:02
  • Also worth noting the OP does not want to go from 18 to 15, just to fix the casing. – Adrian Larson May 24 '16 at 20:13
  • @AdrianLarson Fix to use in the URL or are there other contexts where there is case sensitivity? – Keith C May 24 '16 at 20:18
  • Hard to say why. The two most obvious cases I've seen are in any url or query. It would also affect comparison, but it's hard to see how that would come into play. – Adrian Larson May 24 '16 at 20:20
  • 2
    Purpose here was actually querying for the record indeed! – Koen Faro May 24 '16 at 20:40
  • @KoenFaro Maybe my misunderstanding - I though the query will work whatever the casing of the 18 characters, kinda the whole point. – Keith C May 24 '16 at 20:42
  • 1
    Thats what I assumed too @KeithC, until I noticed that my test-case only had uppercase characters to start with and things broke down in production! – Koen Faro May 24 '16 at 20:50
  • 1
    @KoenFaro OK I did a bit more reading and I get it now. Correctly cased 18 chars or 15 chars work in queries but if case is lost code needs running to use the extra info in the extra 3 character os the 18 chars to recover the correct casing of the 15 chars. Seems like a method that should be present on the Id class but isn't. – Keith C May 24 '16 at 21:01
  • 1
    I would actually expect it to be part of the native Id.valueOf function, rather strange that SF skipped implementing the few lines of code above, sounds like a rush job of adding 18 character Ids - if you open the idea I'll vote! – Koen Faro May 24 '16 at 21:09
  • 1
    @KoenFaro If I believed opening developer-oriented ideas made any difference I would... – Keith C May 24 '16 at 21:27
  • Open. Though I agree it's probably moot. – Adrian Larson May 24 '16 at 21:32
  • @AdrianLarson Voted for by me. – Keith C May 24 '16 at 22:18
  • 1
    Peter Chittum provided some insight to me a while ago on this very point - casing does matter, always. http://salesforce.stackexchange.com/a/50483/660 – Mark Pond May 24 '16 at 23:10
1

In case anyone else is using it, I took Adrian Larson's apex code and converted it to Python.

def getBitPatterns(c):
    CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ012345'
    index = CHARS.find(c)
    result = []
    for bitNumber in range(0,5):
        result.append((index & (1 << bitNumber)) != 0)
    return result

def repairCasing(inp):
    if len(inp) < 18:
        return 'Error'
    toUpper = []
    toUpper.append(getBitPatterns(inp[15:16]))
    toUpper.append(getBitPatterns(inp[16:17]))
    toUpper.append(getBitPatterns(inp[17:18]))
    toUpper = [item for sublist in toUpper for item in sublist]

    output = ''

    for i in range(0,15):
        c = inp[i:i+1]
        if toUpper[i]:
            output += c.upper()
        else:
            output += c.lower()

    output += inp[15:18].upper()

    return output
Jwok
  • 767
  • 1
  • 11
  • 32
  • 1
    I think you can get the last 12 lines down to one with a list comprehension if you like. return ''.join([x.upper() if x in toUpper else x.lower() for x in inp[:15]] + inp[15:].upper() – David Reed Nov 30 '18 at 19:24
  • I messed it up, of course. in toUpper should just be toUpper[x]. – David Reed Dec 01 '18 at 20:35
1

If anyone needs to do this with PHP, I ported Adrian's answer. Requires PHP 8.0 or newer:

<?php

declare(strict_types=1);

namespace Shadowhand\Salesforce;

use function assert; use function strlen; use function strpos; use function strtolower; use function strtoupper; use function substr;

/**

  • @link https://salesforce.stackexchange.com/questions/123217/converting-uppercased-18-digit-id-to-valid-id

*/ final class SalesforceId { private const CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ012345';

public static function correct(string $input): string
{
    assert(assertion: strlen($input) === 18);

    $id = '';

    $capitalize = [
        ...self::shouldCapitalize(letter: substr($input, offset: 15, length: 1)),
        ...self::shouldCapitalize(letter: substr($input, offset: 16, length: 1)),
        ...self::shouldCapitalize(letter: substr($input, offset: 17, length: 1)),
    ];

    for ($i = 0; $i &lt; 15; $i++) {
        $letter = substr($input, offset: $i, length: 1);

        $id .= $capitalize[$i] ? strtoupper($letter) : strtolower($letter);
    }

    $id .= strtoupper(substr($input, offset: 15, length: 3));

    return $id;
}

private static function shouldCapitalize(string $letter): array
{
    $index = strpos(haystack: self::CHARS, needle: strtoupper($letter));
    $result = [];

    for ($bit = 0; $bit &lt; 5; $bit++) {
        $result[] = ($index &amp; (1 &lt;&lt; $bit)) !== 0;
    }

    return $result;
}

private function __construct()
{
}

}

And here is the PHPUnit test:

<?php

declare(strict_types=1);

namespace Shadowhand\Salesforce;

use PHPUnit\Framework\TestCase;

use function strtolower; use function strtoupper;

class SalesforceIdTest extends TestCase { public function testShouldCorrectCapitalization(): void { $id = '00129000007Kbn7AAC';

    self::assertSame(expected: $id, actual: SalesforceId::correct(strtoupper($id)));
    self::assertSame(expected: $id, actual: SalesforceId::correct(strtolower($id)));
}

}

shadowhand
  • 111
  • 2