What is the problem you're trying to solve?

You've heard it said. But when you're deep in code and wires and get stuck, it's hard to write a question with the express aim of making it easy for people to answer.

I'm a big fan of the Arduino Forum. Before falling in love with Arduino I had not been part of such a lively, intelligent and, frankly, very well behaved online community. The people who frequent the forum really do "preach the gospel of Arduino with love." Yes, I said with love because they do spend a lot of time and effort explaining why they love Arduino and giving visitors every opportunity to love it too.

But when we go to the forum and blurt out our disorganised thoughts, we are really just shooting ourselves in the foot. Yes, the God Members will post to your thread. Yes they'll ask questions and request you post your code (and use code tags!). But this is a pattern that must drive them crazy. I would suggest the majority of questions on the form don't answer the vital question - "what is the problem you're trying to solve?" Every time we post, we should re-read our own writing until we we're quite sure the next person to read it understands the problem you're trying to solve. I've had my soap-box moment. Now on to my problem.

I'm receiving data from my GPS and it's all text. Most of the text represents numbers, but these have to be converted somehow befiore I can perform mathematical operations on them. My first concern is that I need to correct the time for my time zone. How do I add 11 hours to the UTC time? I've seen various solutions to this, most involving atoi() which isn't a core Arduino function. It's "inherited" (not the correct term, I'm sure) from a C library for AVR microcontrollers. The offical avr-libc documentation reads thus:

atoi(): Convert a string to an integer. The atoi() function converts the initial portion of the string pointed to by s to integer representation.

I'm sorry, what? Forgetting the fact that non-programmers probably haven't made it this far to even find the documentation, how's this for terse? The int data type can store values up to 32,767 so, fair enough, atoi() looks like it ignores more than 5 characters in the input string. Maybe this is what's meant by the "initial portion." Then again, int can also store negative values down to -32,768 so perhaps it's 6 characters that are included. So, I'm fairly sure if I do atoi("32000") I'll get the number 32000. But what happens if I give it something with a valid number of characters that's out-0f-range, like atoi("65000")? I'm already over it. I'd much rather create my own library to handle this stuff than prod and poke someone else's function to try to understand what it does.

I began a Number class with the initial goal of being able to convert simple strings to integral data types. Yes, that's "strings" with a lower-case S. I'm referring to arrays of characters with a null delineating the end of the string. I'm not using the String data type, but hope to incorporate that later.

I've created a stand-alone Arduino project, with the main sketch acting as a test suite for the attached String class. Here's the header file...

Edit: Code now available for download on this link.



Number.h

#ifndef Number_h
#define Number_h

#include "WProgram.h"

class Number
{
public:
static boolean parseChar(const char& input, byte& output);
static boolean parseCharArray(char* input, byte& output);
static boolean parseCharArray(char* input, int& output);
static boolean parseCharArray(char* input, unsigned int& output);
static boolean parseCharArray(char* input, long& output);
static boolean parseCharArray(char* input, unsigned long& output);
private:
static int signOf(char*& in);
};

#endif

So now I find 0021 of Arduino has lost the Copy as HTML function. Sheesh.

As you can see, the function parseCharArray() does all the work, and is overloaded. By overloaded I don't mean it's getting tired, I mean it has various signatures based on the data type it's returning (the output parameter). The function returns a boolean (true or false) to let you know if it succeeded in converting your string to a number, or it failed. It can be used this way:

int myNumber;
if (parseCharArray("1234", myNumber))
{
// put code here for when myNumber is valid.
}
else
{
// put code here for when myNumber is not valid.
}

Apologies for just banging all the code in here. I'll have to get a file hosting thang set up soon.




Number.cpp


#include "Number.h"

#define BYTE_MAX 255
#define INT_MAX 32767
#define INT_MAX_NEG 32768
#define UNSIGNED_INT_MAX 65535
#define LONG_MAX 2147483647
#define LONG_MAX_NEG 2147483648
#define UNISIGNED_LONG_MAX_DIV_10 429496729 // 4,294,967,295 actual max.

// Converts an ASCII character to a numeric value.
// Returns true if the character is numeric, returns
// false for all other characters.
// Utilises the fact that char type is just a byte.
// Thus '7' (ASCII 55) minus '0' (ASCII 48) equals 7.
boolean Number::parseChar(const char& input, byte& output)
{
if(input >= '0' && input <= '9') { output = input - '0'; return true; } output = 0; return false; } // Converts a string (null-terminated character array) // into an integral value. The string may have a leading // minus sign for negative values. // Returns true if the string is successfully converted // to a number. False otherwise. // Any non-numeric characters in the input, other than // minus and null terminator, will cause failure. boolean Number::parseCharArray(char* input, byte& output) { int negation = signOf(input); if (negation < integer =" 0;"> BYTE_MAX)) return false;
// not a number or number out of range.

output = (byte) integer;
return true; // successful conversion.
}


// Converts a string (null-terminated character array)
// into an integral value. The string may have a leading
// minus sign for negative values.
// Returns true if the string is successfully converted
// to a number. False otherwise.
// Any non-numeric characters in the input, other than
// minus and null terminator, will cause failure.
boolean Number::parseCharArray(char* input, int& output)
{
int negation = signOf(input);
unsigned long integer = 0;
if (!parseCharArray(input, integer)) return false; // not a number.

if (negation > 0)
{
if(integer > INT_MAX) return false;
}
else
{
if(integer > INT_MAX_NEG) return false;
}
output = (int) integer * negation;
return true; // successful conversion.
}

// Converts a string (null-terminated character array)
// into an integral value. The string may have a leading
// minus sign for negative values.
// Returns true if the string is successfully converted
// to a number. False otherwise.
// Any non-numeric characters in the input, other than
// minus and null terminator, will cause failure.
boolean Number::parseCharArray(char* input, unsigned int& output)
{
int negation = signOf(input);
if (negation < integer =" 0;"> UNSIGNED_INT_MAX) return false; // out of range
output = (unsigned int) integer;
return true; // successful conversion.
}

// Converts a string (null-terminated character array)
// into an integral value. The string may have a leading
// minus sign for negative values.
// Returns true if the string is successfully converted
// to a number. False otherwise.
// Any non-numeric characters in the input, other than
// minus and null terminator, will cause failure.
boolean Number::parseCharArray(char* input, long& output)
{
int negation = signOf(input);
unsigned long integer = 0;
if (!parseCharArray(input, integer)) return false; // not a number.

if (negation > 0)
{
if(integer > LONG_MAX) return false; // out of range.
}
else
{
if(integer > LONG_MAX_NEG) return false; // out of range.
}
output = (long) integer * negation;
return true; // successful conversion.
}

// Converts a string (null-terminated character array)
// into an integral value. The string may have a leading
// minus sign for negative values.
// Returns true if the string is successfully converted
// to a number. False otherwise.
// Any non-numeric characters in the input, other than
// minus and null terminator, will cause failure.
boolean Number::parseCharArray(char* input, unsigned long& output)
{
byte digit;
unsigned long result = 0;
unsigned long previousResult = 0;

while(*input != '\0')
{
if(parseChar(*(input++), digit))
{
// Maximum value of unsigned long is 4294967295.
// Unsigned long does not overflow on multiplication
// as it does on addition.
// 4294967295 + 10 = 9 Recycles on overflow as expected.
// 4294967295 * 10 = 4294967286 (= 4294967295 - 10) ????
// 500000000 * 10 = 705032704 ????
// Therefore, need different handling for multiplication
// and addition overflows.
if (result > UNISIGNED_LONG_MAX_DIV_10) return false; // anticipates multiplication overflow
result *= 10;
result += digit;
}
else return false; // non-numeric character found.
if (previousResult > result) return false; // addition overflow has occurred.
else previousResult = result;
}
output = result;
return true; // successful conversion.
}

// Determines if the first character in a string
// is a minus sign.
// Returns -1 if the character is a minus; 1 otherwise.
// Passes character pointer by reference so that the pointer
// in the calling function can be moved to the first numeric
// digit in the string if a minus sign exists.
int Number::signOf(char*& input)
{
if (*input == '-')
{
input++;
return -1;
}
return 1;
}



Number_98.pde


#include "Number.h"

void setup()
{
Serial.begin(115200);
Serial.println("Beginning Test Suite");
Serial.println();

// unsigned data type tests
testFunction("0", (byte) 0, true); // zero test
testFunction("1", (byte) 1, true); // base case
testFunction("123", (byte) 123, true); // random in-range case
testFunction("255", (byte) 255, true); // edge case
testFunction("256", (byte) 256, false); // just out-of-range cases +/-
testFunction("-1", (byte) 0, false);
testFunction("1000", (byte) 0, false); // extreme out-of-range cases +/-
testFunction("-1000", (byte) 0, false);
Serial.println();

// signed data type tests
testFunction("0", (int) 0, true); // zero test
testFunction("1", (int) 1, true); // base cases +/-
testFunction("-1", (int) -1, true);
testFunction("1001", (int) 1001, true); // random in-range cases +/-
testFunction("-1001", (int) -1001, true);
testFunction("32767", (int) 32767, true); // edge cases +/-
testFunction("-32768", (int) -32768, true);
testFunction("32768", (int) 0, false); // just out of range cases +/-
testFunction("-32769", (int) 0, false);
testFunction("4000000", (int) 4000000, false); // extreme out-of-range cases +/-
testFunction("-4000000", (int) -4000000, false);
Serial.println();

testFunction("0", (unsigned int) 0, true); // zero test
testFunction("1", (unsigned int) 1, true); // base case
testFunction("123", (unsigned int) 123, true); // random in-range case
testFunction("65535", (unsigned int) 65535, true); // edge case
testFunction("65536", (unsigned int) 0, false); // just out-of-range cases +/-
testFunction("-1", (unsigned int) 0, false);
testFunction("100000", (unsigned int) 0, false); // extreme out-of-range cases +/-
testFunction("-100000", (unsigned int) 0, false);
Serial.println();

testFunction("0", (long) 0, true); // zero test
testFunction("1", (long) 1, true); // basic case +/-
testFunction("-1", (long) -1, true);
testFunction("1001", (long) 1001, true); // random in-range cases +/-
testFunction("-1001", (long) -1001, true);
testFunction("2147483647", (long) 2147483647, true); // edge cases +/-
testFunction("-2147483648", (long) -2147483648, true);
testFunction("2147483648", (long) 0, false); // just out of range cases +/-
testFunction("-2147483649", (long) 0, false);
testFunction("40000000000", (long) 0, false); // extreme out-of-range cases +/-
testFunction("-40000000000", (long) 0, false);
Serial.println();

testFunction("0", (unsigned long) 0, true); // zero test
testFunction("1", (unsigned long) 1, true); // basic case +/-
testFunction("123456789", (unsigned long) 123456789, true); // random in-range case
testFunction("4294967295", (unsigned long) 4294967295, true); // edge case
testFunction("4294967296", (unsigned long) 0, false); // just out-of-range cases +/-
testFunction("-1", (unsigned long) 0, false);
testFunction("50000000000", (unsigned long) 0, false); // extreme out-of-range cases +/-
testFunction("-50000000000", (unsigned long) 0, false);
Serial.println();

// Fuzzing Cases
testFunction("000", (unsigned long) 0, true); // zero test, odd but valid format
testFunction("001", (unsigned long) 1, true); // base test, odd but valid format
testFunction("000-", (unsigned long) 0, false); // zero test, invalid format
testFunction("001a", (unsigned long) 0, false); // base test, invalid format
testFunction("1.1", (unsigned long) 0, false); // valid real number, invalid integer
testFunction("12 3", (unsigned long) 0, false); // number with spaces, invalid format.
testFunction(" 123", (unsigned long) 0, false); // number with spaces, invalid format.
testFunction("(*&@!#", (unsigned long) 0, false); // garbage data.
}

void loop()
{
}

void testFunction(char* input, byte expectedValue, boolean expectedResult)
{
byte actualValue = 0;
boolean actualResult = Number::parseCharArray(input, actualValue);

Serial.print("byte Number::parseCharArray(");
Serial.print(input);
Serial.print(", ) -> ");

// failed as expected = pass.
if(!expectedResult && !actualResult)
{
Serial.println("Pass");
return;
}

// passed with correct value
if((actualResult == expectedResult) && (actualValue == expectedValue))
{
Serial.println("Pass");
return;
}
Serial.print("FAIL! actual (expected) Result: ");
printBoolean(actualResult);
Serial.print(" (");
printBoolean(expectedResult);
Serial.print(") Value: ");
Serial.print(actualValue, DEC);
Serial.print(" (");
Serial.print(expectedValue, DEC);
Serial.print(")");
Serial.println();
}

void testFunction(char* input, int expectedValue, boolean expectedResult)
{
int actualValue = 0;
boolean actualResult = Number::parseCharArray(input, actualValue);

Serial.print("int Number::parseCharArray(");
Serial.print(input);
Serial.print(", ) -> ");

// failed as expected = pass.
if(!expectedResult && !actualResult)
{
Serial.println("Pass");
return;
}

// passed with correct value
if((actualResult == expectedResult) && (actualValue == expectedValue))
{
Serial.println("Pass");
return;
}
Serial.print("FAIL! actual (expected) Result: ");
printBoolean(actualResult);
Serial.print(" (");
printBoolean(expectedResult);
Serial.print(") Value: ");
Serial.print(actualValue, DEC);
Serial.print(" (");
Serial.print(expectedValue, DEC);
Serial.print(")");
Serial.println();
}

void testFunction(char* input, unsigned int expectedValue, boolean expectedResult)
{
unsigned int actualValue = 0;
boolean actualResult = Number::parseCharArray(input, actualValue);

Serial.print("unsigned int Number::parseCharArray(");
Serial.print(input);
Serial.print(", ) -> ");

// failed as expected = pass.
if(!expectedResult && !actualResult)
{
Serial.println("Pass");
return;
}

// passed with correct value
if((actualResult == expectedResult) && (actualValue == expectedValue))
{
Serial.println("Pass");
return;
}
Serial.print("FAIL! actual (expected) Result: ");
printBoolean(actualResult);
Serial.print(" (");
printBoolean(expectedResult);
Serial.print(") Value: ");
Serial.print(actualValue, DEC);
Serial.print(" (");
Serial.print(expectedValue, DEC);
Serial.print(")");
Serial.println();
}

void testFunction(char* input, long expectedValue, boolean expectedResult)
{
long actualValue = 0;
boolean actualResult = Number::parseCharArray(input, actualValue);

Serial.print("long Number::parseCharArray(");
Serial.print(input);
Serial.print(", ) -> ");

// failed as expected = pass.
if(!expectedResult && !actualResult)
{
Serial.println("Pass");
return;
}

// passed with correct value
if((actualResult == expectedResult) && (actualValue == expectedValue))
{
Serial.println("Pass");
return;
}
Serial.print("FAIL! actual (expected) Result: ");
printBoolean(actualResult);
Serial.print(" (");
printBoolean(expectedResult);
Serial.print(") Value: ");
Serial.print(actualValue, DEC);
Serial.print(" (");
Serial.print(expectedValue, DEC);
Serial.print(")");
Serial.println();
}

void testFunction(char* input, unsigned long expectedValue, boolean expectedResult)
{
unsigned long actualValue = 0;
boolean actualResult = Number::parseCharArray(input, actualValue);

Serial.print("unsigned long Number::parseCharArray(");
Serial.print(input);
Serial.print(", ) -> ");

// failed as expected = pass.
if(!expectedResult && !actualResult)
{
Serial.println("Pass");
return;
}

// passed with correct value
if((actualResult == expectedResult) && (actualValue == expectedValue))
{
Serial.println("Pass");
return;
}
Serial.print("FAIL! actual (expected) Result: ");
printBoolean(actualResult);
Serial.print(" (");
printBoolean(expectedResult);
Serial.print(") Value: ");
Serial.print(actualValue, DEC);
Serial.print(" (");
Serial.print(expectedValue, DEC);
Serial.print(")");
Serial.println();
}

void printBoolean(boolean value)
{
if (value) Serial.print("TRUE");
else Serial.print("FALSE");
}


Whew. Too much code. Anyway, apologies for the state of the code. HTML flattens all the indentation now that Arduino won't HTMLify it for me. Paste the three files into a new project and test 'em out. I aim to add String support (yes, the class String) and also float and double data types. Then on to Date and Time types.

Please write with any comments you have either positive or negative. All feedback gratefully appreciated.

That is all.

Comments