Edit2: new answer below, but first some background.
I just integrated one of these cheap encoders into a project. They are made in China and resold under many different names. They are mechanical quadrature encoders. They have mechanical detents and they go through all 4 phases/quadrants for each "click" of the knob. The signals are labelled "CLK" and "DT" instead of the normal A/B. Both signals are high at the detents.
If you are sampling the clock signal fast, you need to de-bounce it (or possibly evaluate both signals with a state machine). The worst bounce that I have observed on a scope is 0.2 ms (yes, this is one part, others could be worse, and this one could get worse over time; this is just a home project and an occasional extra count is not serious). I am sampling in a loop that runs at 0.3 ms. When I get a logic 1 sample followed by 2 logic 0 samples then I "accept" the clock edge.
There are many other de-bounce algorithms. Which one I use often depends on the surrounding code. (My standard debounce algorithm is almost exactly as TonyM described in his comment, but the code wouldn't integrate well into this project)
When I turn the knob as fast as I will ever turn it, the "DT" signal will change as soon as 2 ms after the "CLK" signal. So, I needed to be sure to read the direction signal (DT) within this time for it to be valid. Although I am reading both signals simultaneously, since I am only debouncing the Clock, the debounce adds skew that will cause issues if the skew is more than 2 ms. I could put both signals through a debounce algorithm as an alternate solution, but it isn't necessary.
Based on comments from thebusybee, I believe that contact bounce (my original answer) isn't the primary problem. Since you are looking for both edges of the clock, you should expect to get 2 counts per click! The printf is slow, and delays the next sample, so this could be the reason why you don't always see 2 counts per click. More data would need to be collected to be certain.
If you fix the edge detector logic and remove the printf delay, I am certain that you will see the switch bounce issue. So, be prepared for it.
Note that a mechanical encoder is meant to be used for a human interface (knob), or a motor that moves infrequently. If you attach this to a constantly moving motor, you will quickly wear it out.
Also note that the (poor) spec sheet says that "+" must be 5V. It is just the voltage for the pull-ups, use 3.3V if your MCU has 3.3V I/O.
A hardware solution would be an RC filter followed by a Schmitt trigger. If you have complex software performing many tasks while you need to look for knob clicks, then this is probably a better solution. Then, you can put the CLK signal on an interrupt.
One click CW (CLK: Yellow, DT: Blue):

One click CCW (CLK: Yellow, DT: Blue):

Edit1: Added code that works reliably for me. The debounce only adds a few lines of code. I did remove some non-relevant stuff, hopefully, I didn't accidentally remove something essential.
Edit3: Fixed the issue in the debounce code pointed out in the comments. The debounce algorithm could be better, but it is good enough for my purposes.
#define LOOP_DELAY_USEC 300 // Loop Delay
uint8_t u8EncoderPort;
bool bClkState;
bool bPrev1ClkState = 1;
bool bPrev2ClkState = 0;
bool bRotCw;
bool bCountChanged = false;
while ( true )
{
u8EncoderPort = ENCODER_PORT_IN; // Read HW Port
// Extract the Clk and DT bits
bClkState = u8EncoderPort & ENCODER_CLK_BIT;
bRotCw = u8EncoderPort & ENCODER_DT_BIT;
// Look for negative edges (debounce: One high sample, followed by two low samples)
if ( !bClkState && !bPrev1ClkState && bPrev2ClkState )
{
// Increase or decrease count based on quadrature direction
if ( bRotCw )
giEncoderCount++;
else
giEncoderCount--;
bCountChanged = true;
}
bPrev2ClkState = bPrev1ClkState; // Remember the previous states (for debounce)
bPrev1ClkState = bClkState;
if ( bCountChanged )
{
Update Display ...
bCountChanged = false;
}
// Delay 300 uSec
__delay_cycles( LOOP_DELAY_USEC * CLK_MHZ ); // Should be a little longer than the longest switch bounce
// But, rotary encoder debounce must be less than 2 mS,
// else risk getting the direction wrong
}