debouncing rotary encoder - almost there...

I use a Bourns PEC12R rotary encoder in one of my designs, with this datasheet suggested filter. It is used to indicate the file number on an SD card to be accessed, which have a limit of 0 to 99, or 0 to FF, the range is selectable with the HEX_FILE_NAMES switch (limited by a 2 character display).
You could do similar, with the rotary encoder pulses being interpreted in the Interrupt Service Routine to count stuff up and down, where loop() then does something with the data.

Nack Gammon wrote this PCINT code to read the rotary encoder turns with the 328P processor.

volatile boolean fired = false;
const byte Encoder_A_Pin = 8;  // PB0, pin 12 (TQFP) on board
const byte Encoder_B_Pin = 9;  // PB1, pin 13 (TQFP) on board
const unsigned long ROTARY_DEBOUNCE_TIME = 100; // milliseconds

  volatile int fileNumber = 0;

  #if HEX_FILE_NAMES
    const int MAX_FILE_NUMBER = 0xFF;
  #else
    const int MAX_FILE_NUMBER = 99;
  #endif // HEX_FILE_NAMES

In setup:  
// pin change interrupt (example for D9)
  PCMSK0 = bit (PCINT0) | bit (PCINT1);  // want pin 8 and 9
  PCIFR  = bit (PCIF0);   // clear any outstanding interrupts
  PCICR  = bit (PCIE0);   // enable pin change interrupts for D0 to D7

In loop:
    if (fired)
      {
      // debugging display perhaps?
          
      fired = false;
      Serial.println ("tick"); // used to tell if the rotary encoder was clicked or not.

      }  // end if fired

Interrupt Service Routine to handle the PCINTs from the encoder turning:
 // handle pin change interrupt for D8 to D13 here
ISR (PCINT0_vect)
{
static byte pinA, pinB;  
static boolean ready;
static unsigned long lastFiredTime;

  byte newPinA = digitalRead (Encoder_A_Pin);
  byte newPinB = digitalRead (Encoder_B_Pin);
  
  if (pinA == newPinA && 
      pinB == newPinB)
      return;    // spurious interrupt

  // so we only record a turn on both the same (HH or LL)
  
  // Forward is: LH/HH or HL/LL
  // Reverse is: HL/HH or LH/LL

  if (newPinA == newPinB)
    {
    if (ready)
      {        
      if (millis () - lastFiredTime >= ROTARY_DEBOUNCE_TIME)
        {
        if (newPinA == HIGH)  // must be HH now
          {
          if (pinA == LOW)
            fileNumber ++;
          else
            fileNumber --;
          }
        else
          {                  // must be LL now
          if (pinA == LOW)  
            fileNumber --;
          else
            fileNumber ++;        
          }
        if (fileNumber > MAX_FILE_NUMBER)
          fileNumber = 0;
        else if (fileNumber < 0)
          fileNumber = MAX_FILE_NUMBER;
        lastFiredTime = millis ();
        fired = true;
        } 
      ready = false;
      }  // end of being ready
    }  // end of completed click
  else
    ready = true;
    
  pinA = newPinA;
  pinB = newPinB;
    
 }  // end of PCINT2_vect

This demo is a little dark, it shows the filenumber count wrapping around 0 (0 back to FF and down, then back to 0 and up). Note the use of 'volatile' for variables that are used in the ISR.
https://www.youtube.com/watch?v=f51eSlcZt-g
Bourns PEC12R rotary encoder filter.JPG

Bourns PEC12R rotary encoder filter.JPG