How to program the I/O output values for a 14-segment display driver

I'm trying to program a display driver for a 14-segment display. The idea is that I'll have an MCP23017 16-bit IO expander which drives the display. I'll write a 16-bit value to this chip which encodes the pins that are on or off for each character. Now I need a way to link the characters ('0', '1', 'a', 'b', 'c', etc...) to their corresponding bitstring (0b0100111010001010 for example). I'm used to programming within powerful systems instead of microchips so my first thought was to use a hashmap/associative array kind of thing. When a character is passed to the driver, the driver looks for the value it needs to send to the IO expander to display that given character and sends it on its way.

I have tried looking for map implementations for Arduino without success. Everything I find either isn't suited or doesn't compile. There's also no way I have time to write an implementation myself. So I'm looking for another solution. Preferably one that's expandable but still compact and short to write.

The only thing I could come up with so far is to use the ascii value of each character for easy lookup but that still leaves me clueless as to how to link the ascii value to the output value.

I should add that I'm an experienced programmer but I'm definitely not experienced with C/C++. This language.. it's a massive riddle to me. Things like structs and templates don't mean a thing to me (except that I know they exist and that I should probably be using them for this).

Fourteen segments, so use a table (simple lookup table - an array) of uint16_t, indexed by the ASCII value of the character to be displayed.

Save RAM and use PROGMEM instead.

Have an array with expected characters.
Have a corresponding array with output segments.
The offset of the first array then is used to offset into the second array.
Refer to look up table for more information.

So you're saying two arrays (for example array1 = {'0', '1', '2', 'a', 'b', 'c'} and array2 = {20303, 1211, 23234, 238, 0, 65536}). The driver gets to process a '2' so it looks in array1, finds it at i = 2, then finds the value at i = 2 in array2 and sends 23234 to the display. Did I understand you correctly?

Is it efficient to do a linear search in array1? The array won't be big (something like 30-40 characters max) but it still feels a bit wrong...

Why two arrays?
It's a simple single dimensional array.

Oh right, you meant just using the nested array as a key-value pair? And then loop over the 'keys' linearly?

If the chars expected are not ascii, then you are right, one would be needed.

I was thinking of IR codes the other day.

The following code segment 'is not' what you want but should give you an idea of something similar.
Also, as mentioned already, placing your array in PROGMEM is the way to save on RAM.

// Available Character patterns
//
//                   .abcdefg
//                   76543210
const byte chr0  = 0b01111110;  // 0
const byte chr1  = 0b00110000;  // 1
const byte chr2  = 0b01101101;  // 2
const byte chr3  = 0b01111001;  // 3
const byte chr4  = 0b00110011;  // 4
const byte chr5  = 0b01011011;  // 5
const byte chr6  = 0b01011111;  // 6
const byte chr7  = 0b01110000;  // 7
const byte chr8  = 0b01111111;  // 8
const byte chr9  = 0b01111011;  // 9

//                    .abcdefg 
//                    76543210  
const byte chrA   = 0b01110111;  // A
const byte chra   = 0b11100111;  // a
const byte chrB   = 0b01111111;  // B
const byte chrb   = 0b00011111;  // b
const byte chrc   = 0b00001101;  // c
const byte chrC   = 0b01001110;  // C
const byte chrD   = 0b01111110;  // D
const byte chrd   = 0b00111101;  // d
const byte chrE   = 0b01001111;  // E
const byte chre   = 0b01101111;  // e
const byte chrF   = 0b01000111;  // F
const byte chrG   = 0b01011111;  // G
const byte chrg   = 0b01111011;  // g
const byte chrH   = 0b00110111;  // H
const byte chrh   = 0b00010111;  // h
const byte chrI   = 0b00110000;  // I
const byte chri   = 0b00010000;  // i
const byte chrJ   = 0b01111100;  // J
const byte chrK   = 0b10100111;  // K
const byte chrj   = 0b00111000;  // j
const byte chrL   = 0b00001110;  // L
const byte chrl   = 0b00110000;  // l
const byte chrM   = 0b11111001;  // M
const byte chrn   = 0b00010101;  // n
const byte chrO   = 0b01111110;  // O
const byte chro   = 0b00011101;  // o
const byte chrP   = 0b01100111;  // P
const byte chrq   = 0b11110011;  // q
const byte chrR   = 0b01110111;  // R
const byte chrr   = 0b00000101;  // r
const byte chrS   = 0b01011011;  // S
const byte chrt   = 0b00001111;  // t
const byte chrU   = 0b00111110;  // U
const byte chru   = 0b00011100;  // u
const byte chrW   = 0b11001111;  // W
const byte chrX   = 0b10100111;  // X
const byte chrY   = 0b00111011;  // Y
const byte chrZ   = 0b11100101;  // Z

//                     .abcdefg 
//                     76543210  
const byte chrSP   = 0b00000000; //    SPace 
const byte chrEM   = 0b10110000; // !  Exclamation Mark
const byte chrQU   = 0b01000010; // "  QUotes
const byte chrAP   = 0b01000000; // '  APostrophe
const byte chrLB   = 0b01001110; // [  Left Bracket
const byte chrRB   = 0b01111000; // ]  Right Bracket
const byte chrDP   = 0b10000000; // .  Decimal Point
const byte chrQM   = 0b01100101; // ?  Question Mark
const byte chrEQ   = 0b01000001; // =  EQual
const byte chrBS   = 0b00010011; // \  Back Slash
const byte chrFS   = 0b00100101; // /  Forward Slash

//                     .abcdefg 
//                     76543210
const byte chrSDP  = 0b10000000; // .  Segment dp 
const byte chrSA   = 0b01000000; //    Segment A  
const byte chrSB   = 0b00100000; //    Segment B 
const byte chrSC   = 0b00010000; //    Segment C 
const byte chrSD   = 0b00001000; // _  Segment D 
const byte chrSE   = 0b00000100; //    Segment E
const byte chrSF   = 0b00000010; //    Segment F 
const byte chrSG   = 0b00000001; // -  Segment G 


//    --A--
//  |       |
//  F       B
//  |       |
//    --G--
//  |       |
//  E       C
//  |       |
//    --D--
//           (DP)
//
//================================================================

// Defined words
byte temp[8];
byte buffer[32] = {
  chrSP,chrSP,chrSP,chrSP,chrSP,chrSP,chrSP,chrSP,
  chrA ,chrr ,chrd ,chru ,chri ,chrn ,chro ,chrEM,
  chrSP,chrSP,chrr ,chru ,chrl ,chre ,chrS+chrDP,chrSP,
  chrSP,chrSP,chrSP,chrSP,chrSP,chrSP,chrSP,chrSP};  // Arduino!  rules.

byte Bubblelicious[32] = {
  chrSP,chrSP,chrSP,chrSP,chrSP,chrSP,chrSP,chrSP,
  chrB ,chrU ,chrB ,chrB ,chrL ,chrE ,chrSG,chrL ,
  chrI ,chrC ,chrI ,chrO ,chrU ,chrS ,chrSP,chrSP,
  chrSP,chrSP,chrSP,chrSP,chrSP,chrSP,chrSP,chrSP};  // BUBBLE-LICIOUS
  
byte ArduinoEM[8] = {
  chrA,chrr,chrd,chru,chri,chrn,chro,chrEM};                 //Arduino!
byte ErrorEMEMEM[8] = {
  chrE,chrr,chrr,chro,chrr,chrEM,chrEM,chrEM};               //Error!!!
byte Bobuino2[8] = {
  chrB,chro,chrb,chru,chri,chrn,chro,chr2};                  //Bobuino2
byte Freq [8] = {
  chrF,chrSP,chrSP,chrSP,chr1+chrDP,chr0,chr1,chr5};         //F   1015
byte PulseH [8] = {
  chrH,chr9+chrDP,chr8,chr5,chr2+chrDP,chr2,chr1,chr0};      //H9852210
byte PulseL [8] = {
  chrL,chr9+chrDP,chr8,chr5,chr2+chrDP,chr2,chr3,chr5};      //L9852235

Okay, I think I get it. I'm building a two-dimensional array now with all the characters I have. Thanks for the help!