Better way to reference group of arrays

I am building a clock that uses a 20x4 LCD display. I want to use custom characters to display 4x4 numbers. I am repeating a lot of things here, which leads me to believe there are some better ways:

#include <Wire.h>
#include <LiquidCrystal_I2C.h>

#if defined(ARDUINO) && ARDUINO >= 100
#define printByte(args)  write(args);
#else
#define printByte(args)  print(args,BYTE);
#endif
//create custom characters
uint8_t topBlock[8]  = {0x0E,0x0E,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F};
uint8_t middleBlock[8] = {0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F};
uint8_t blankBlock[8] = {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00};
uint8_t colon[8] = {0x00,0x00,0x0E,0x0E,0x1F,0x1F,0x00,0x00};
uint8_t eightTop[8]={0x00,0x00,0x00,0x00,0x0E,0x0E,0x1F,0x1F};
uint8_t eightBot[8]={0x1F,0x1F,0x00,0x00,0x00,0x00,0x00,0x00};


//create the layout of each digit (4x4 of custom characters)
uint8_t num0[16] = {2,2,2,2,
                    1,0,0,1,
                    1,0,0,1,
                    1,1,1,1};
uint8_t num1[16] ={0,0,0,2,
                   0,0,2,1,
                   0,0,0,1,
                   0,0,0,1};
uint8_t num2[16] ={2,2,2,2,
                   0,0,4,1,
                   2,1,5,0,
                   1,1,1,1};                  
uint8_t num3[16] ={2,2,2,2,
                   0,4,4,1,
                   0,5,5,1,
                   2,1,1,1};
uint8_t num4[16] ={2,0,0,2,
                   1,0,0,1,
                   1,2,2,1,
                   0,0,0,1};
uint8_t num5[16] ={2,2,2,2,
                   1,4,4,0,
                   0,5,5,2,
                   2,1,1,1};
uint8_t num6[16] ={2,0,0,0,
                   1,2,2,2,
                   1,0,0,1,
                   1,2,2,1};
uint8_t num7[16] ={2,2,2,2,
                   0,0,4,1,
                   0,2,5,0,
                   0,1,0,0};  
uint8_t num8[16] ={2,2,2,2,
                   1,4,4,1,
                   1,5,5,1,
                   1,2,2,1};
uint8_t num9[16] ={2,2,2,2,
                   1,0,0,1,
                   1,2,2,1,
                   0,0,0,1};                                           
                    
  
LiquidCrystal_I2C lcd(0x27,20,4); 

void setup()
{
  lcd.init();                      // initialize the lcd 
  lcd.backlight();
  
  lcd.createChar(2, topBlock);
  lcd.createChar(1, middleBlock);
  lcd.createChar(0, blankBlock);
  lcd.createChar(3, colon);
  lcd.createChar(4, eightTop);
  lcd.createChar(5, eightBot);

  lcd.home();
  
  
}

void loop()
{
  printNumber(0,1);
  delay(2000);
  printNumber(1,1);
  delay(2000)
}

void printNumber(uint8_t num, uint8_t posn) {
 //allow the digit to be placed in the three positions across the screen, the first spot is a skinny version of number 1
 uint8_t digitOffset = 0;
  if (posn == 1) digitOffset = 4;
  else if (posn == 2) digitOffset = 11;
  else if (posn == 3) digitOffset = 16;
  
  if (num == 0) {
    lcd.setCursor(digitOffset,0);
    for (uint8_t i = 0; i < 4; i++) {
      lcd.printByte(num0[i]);
    }
    lcd.setCursor(digitOffset,1);
    for (uint8_t i = 4; i < 8; i++) {
      lcd.printByte(num0[i]);
    }
    lcd.setCursor(digitOffset,2);
    for (uint8_t i = 8; i < 12; i++) {
      lcd.printByte(num0[i]);
    }
    lcd.setCursor(digitOffset,3);
    for (uint8_t i = 12; i < 16; i++) {
      lcd.printByte(num0[i]);
    }
  }
  else if (num == 1) {
    lcd.setCursor(digitOffset,0);
    for (uint8_t i = 0; i < 4; i++) {
      lcd.printByte(num1[i]);
    }
    lcd.setCursor(digitOffset,1);
    for (uint8_t i = 4; i < 8; i++) {
      lcd.printByte(num1[i]);
    }
    lcd.setCursor(digitOffset,2);
    for (uint8_t i = 8; i < 12; i++) {
      lcd.printByte(num1[i]);
    }
    lcd.setCursor(digitOffset,3);
    for (uint8_t i = 12; i < 16; i++) {
      lcd.printByte(num1[i]);
    }
  }
}

Your sketch did not compile :astonished:

Sometimes a binary definition is made for custom characters, then you can see the pattern in the source code.
The custom characters could be in a multi-dimension array.
The 4x4 characters could be in a multi-dimension array, I'm thinking about three dimensions, a group of 4 bytes for the row, then 4 columns, then 9 numbers.
Your blank block is the same as a space.
If you have 7 or less custom characters, then avoid using number 0. It is not possible to put a 0 inside a text string.
The arrays could be made 'const'.
The arrays could be placed in PROGMEM.
Printing a number should be just one piece of code that can print any number.

I don't know if SRAM will be a problem and which Arduino board you use, so I don't know of PROGMEM is appropriate.

For the others, if you want to see what the sketch is displaying (the sketch of the top post, not improved):

You can store the layouts for the digits in a two-dimensional array:

//create the layout of each digit (4x4 of custom characters)
uint8_t nums[10][16] = {
  {2, 2, 2, 2, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1}, //0
  {0, 0, 0, 2, 0, 0, 2, 1, 0, 0, 0, 1, 0, 0, 0, 1}, //1
  {2, 2, 2, 2, 0, 0, 4, 1, 2, 1, 5, 0, 1, 1, 1, 1}, //2
  {2, 2, 2, 2, 0, 4, 4, 1, 0, 5, 5, 1, 2, 1, 1, 1}, //3
  {2, 0, 0, 2, 1, 0, 0, 1, 1, 2, 2, 1, 0, 0, 0, 1}, //4
  {2, 2, 2, 2, 1, 4, 4, 0, 0, 5, 5, 2, 2, 1, 1, 1}, //5
  {2, 0, 0, 0, 1, 2, 2, 2, 1, 0, 0, 1, 1, 2, 2, 1}, //6
  {2, 2, 2, 2, 0, 0, 4, 1, 0, 2, 5, 0, 0, 1, 0, 0}, //7
  {2, 2, 2, 2, 1, 4, 4, 1, 1, 5, 5, 1, 1, 2, 2, 1}, //8
  {2, 2, 2, 2, 1, 0, 0, 1, 1, 2, 2, 1, 0, 0, 0, 1}  //9
};

Then use the number being displayed as the index for the first dimension of the array:

  if (num <= 9) { //verify value of num is valid
    lcd.setCursor(digitOffset, 0);
    for (uint8_t i = 0; i < 4; i++) {
      lcd.printByte(nums[num][i]);
    }
    lcd.setCursor(digitOffset, 1);
    for (uint8_t i = 4; i < 8; i++) {
      lcd.printByte(nums[num][i]);
    }
    lcd.setCursor(digitOffset, 2);
    for (uint8_t i = 8; i < 12; i++) {
      lcd.printByte(nums[num][i]);
    }
    lcd.setCursor(digitOffset, 3);
    for (uint8_t i = 12; i < 16; i++) {
      lcd.printByte(nums[num][i]);
    }
  }

This can also be shorted by using nested for loops:

  if (num <= 9) { //verify value of num is valid
    for (uint8_t row = 0; row < 4; row++) {
      lcd.setCursor(digitOffset, row);
      for (uint8_t column = 0; column < 4; column++) {
        lcd.printByte(nums[num][row * 4 + column]);
      }
    }
  }

There is a way around that limitation. Because of the way the HD44780 LCD controller stores custom characters, they can be addressed using 0 through 7, and also 8 through 15, allowing you to embed character 0 into a string using \10 for octal 8 instead of \0.

The three-dimensional array could be this:

const byte nums[10][4][4] = 
{
  {                   // 0
    { 2, 2, 2, 2 },
    { 1, 0, 0, 1 },
    { 1, 0, 0, 1 },
    { 1, 1, 1, 1 },
  },
  {                   // 1
    { 0, 0, 0, 2 },
    { ... and so on
};

Thank you, the 2D array is perfect for the this.