Getting the nth digit of a number

I am currently working with a 4x 7-segment LED array and I’m trying to display numbers on it.
For this I need to code a function which returns the n-th digit of a number. I want my function to return 0 when the number is smaller than the requested digit. On another post (https://forum.arduino.cc/index.php?topic=524202.0) I found this “formula”:

int ones = n % 10;
int tens = (n / 10) % 10;
int hund = (n / 100) % 10;
int thou = (n / 1000) % 10;
int myri = (n / 10000) % 10;

I put this into a simple function:

int nthdigit(const int number, const int digit) {
  return ((int) trunc(number / ((int) pow(10, digit)))) % 10;
}

And with #include <cmath> on a normal x64 PC with gcc this works as expected.

However if I try this on my ATmega168 with math.h instead of cmath I get weird results. Look at this example:

const int number = 5678;
const int digit = 2;

Serial.println((number / 100) % 10); // results in the expected 6
Serial.println(((int) trunc(number / ((int) pow(10, digit)))) % 10); // also results in the expected 6
Serial.println(nthdigit(number, 2)); // this one gives me a 7

That’s right - if I call the function that does literally the very same thing I did a line above, I get a different result. What could be causing this and how’d I fix it?

Thanks for any help in advance - this is my entire file:

#include <math.h>

int nthdigit(const int number, const int digit) {
  return ((int) trunc(number / ((int) pow(10, digit)))) % 10;
}

void setup() {
  const int number = 5678;
  const int digit = 2;
  Serial.begin(9600);

  Serial.println("-----------");
  Serial.println((number / 100) % 10);
  Serial.println(((int) trunc(number / ((int) pow(10, digit)))) % 10);
  Serial.println(nthdigit(number, 2));
}

void loop() {}
for (int place = 0; place < digit; place++) number = number / 10;
return number % 10;

The following often will not work as you expect, because pow() is computed using floating point numbers and the results are rarely exact integers:

(int) pow(10, digit)

This program:

void setup() {
Serial.begin(9600);
while(!Serial);
for (int i=1; i<6; i++) Serial.println(pow(10,i),6);
}

void loop() {}

prints:

10.000000
99.999992
999.999816
9999.998046
99999.976562

don't use floating point. stick to integers as @pcbbc suggested

J-M-L:
don't use floating point. stick to integers as @pcbbc suggested

Indeed.

If you think performance of multiple divisions by 10 might be a problem, it almost certainly isn’t compared with floating point pow function.
But if you still think it may be an issue, then measure it first. And if it really is a problem, use a lookup table of powers of 10.

Another “transparent” way (hiding the maths) of doing this is to use itoa() to convert the integer into its ASCII representation and then access each char in the array

Thanks for all the answers! I will be testing performance between J-M-L's idea (itoa()) and pccbc's idea with the for loop.

I still have a technical question. Why did the exact same line work when I called it directly, but had precision errors when called via a function?

Thanks again!

scrouthtv:
Why did the exact same line work when I called it directly, but had precision errors when called via a function?

Probably compiler optimisation

scrouthtv:
Thanks for all the answers! I will be testing performance between J-M-L's idea (itoa()) and pccbc's idea with the for loop.

I still have a technical question. Why did the exact same line work when I called it directly, but had precision errors when called via a function?

Thanks again!

It’s not strictly the same code. I’d have to look at the assembler code generated, but my guess would be the compiler is not at all stupid.

When you do this...

const int number = 5678;
const int digit = 2;
Serial.println(((int) trunc(number / ((int) pow(10, digit)))) % 10);

The denominator is a constant expression which is known at compile time.
The compiler can optimise it away.

This however....

const int number = 5678;
const int digit = 2;
Serial.println(nthdigit(number, 2));

Requires a function call (or maybe not if you in-line it, or the compiler decides to do that on your behalf, but let’s assume it doesn’t, or still it isn’t quite clever enough to recognise the constant nature of “digit” in an in-lined function)...
When you make a function call the fact that “digit” is always 2 inside the function is lost.
The function must be able to cope with any value of “digit” it is passed, not just the special case of 2.
So it actually has to call pow.

Edit: Correction. Seems the compiler completely optimises that out as well.

The above lines basically compile to...

Serial.println(6);

This code however cannot be optimised away (unless the compiler loop unrolls, which it seems it doesn't)...

  for (int d = 3; d >= 0; d--)
  {
    Serial.print(nthdigit(number, d));
  }

What code did you actually try? Please present a complete sketch, and say what Arduino platform you are targeting.

That’s my guess anyway. I’m not sure what the C/C++ standards say about precision of optimised expressions and if it is allowed to use a greater precision than would have been used by the function being replaced. Maybe it’s a bit of a bug, maybe it’s allowed to do that.

Fact remains: Avoid using floating point functions/algorithms when integer alternatives are available.

This is my sketch:

#include <math.h>

int nthdigit(const int number, const int digit) {
  return ((int) trunc(number / ((int) pow(10, digit)))) % 10;
}

void setup() {
  const int number = 5678;
  const int digit = 2;
  Serial.begin(9600);

  Serial.println((number / 100) % 10); // 6
  Serial.println(((int) trunc(number / ((int) pow(10, digit)))) % 10); // 6, the "unrolled" function call
  Serial.println(nthdigit(number, 2)); // 7 ??
  // this does   ((int) trunc(number / ((int) pow(10, digit)))) % 10;
  // which is exactly the same as two lines above
}

void loop() {}

I’m running this on an atmel ATmega 168 using MiniCore with a 16 MHz clock.

Copied and pasted the supplied sketch into my IDE.

Compiled and uploaded to ATMega168…

Sketch uses 1798 bytes (12%) of program storage space. Maximum is 14336 bytes.
Global variables use 188 bytes (18%) of dynamic memory, leaving 836 bytes for local variables. Maximum is 1024 bytes.

Output…

6
6
6

IDE 1.8.12

Mine’s a ProMini 3.3V part at 8MHz though. Not that that should make any difference…

Assembler output…

 Serial.println((number / 100) % 10); // 6
 614: 86 e0       ldi r24, 0x06 ; 6
 616: 90 e0       ldi r25, 0x00 ; 0
 618: 0e 94 ba 01 call 0x374 ; 0x374 <_ZN5Print7printlnEii.constprop.3>
  Serial.println(((int) trunc(number / ((int) pow(10, digit)))) % 10); // 6, the "unrolled" function call
 61c: 86 e0       ldi r24, 0x06 ; 6
 61e: 90 e0       ldi r25, 0x00 ; 0
 620: 0e 94 ba 01 call 0x374 ; 0x374 <_ZN5Print7printlnEii.constprop.3>
  Serial.println(nthdigit(number, 2)); // 7 ??
 624: 86 e0       ldi r24, 0x06 ; 6
 626: 90 e0       ldi r25, 0x00 ; 0
 628: 0e 94 ba 01 call 0x374 ; 0x374 <_ZN5Print7printlnEii.constprop.3>

As you can see the compiler has optimised away all the constant expressions and produced 3 consecutive println(6);