Shop OBEX P1 Docs P2 Docs Learn Events
C Language: Create single string from several different variable types — Parallax Forums

C Language: Create single string from several different variable types

I'm trying to do what must be a common objective: save experimental data to SD in CSV format to use in Excel. But I can't find all needed code in tutorials or help examples or StackOverflow
I'm getting several data from each trial.
Data are mix of int, float, char and string.
My approach is to get all values into a string (with rounding, pad and commas) then open SD file and append the single string.
(repeat for each trial)
Examples in Learn C tutorial, from what I see, only save a hard-coded string.

My code list below is long but that is because of careful comments and long variable names.
Problems are at ???, mainly the string length calculation, how to prepare data, how to append each type of datum to string.
/*  StoreDataStringver22.c
Goal:
Concatenate experimental data to string as CSV
Then append to SD file and transfer to Excel

Notes:
  Data types = int, float, char, string
  Goal is the string "dataFullRecord" w/commas
  Data are each fixed length      
Unsolved issues
  round/truncate ints and floats to defined length
  Convert each type to string
  Concatenate
*/

#include "simpletools.h"                      // Include simple tools
int main()                                    // Main function
{
// Vars to hold data for each trial until written
  int datum1_Int;         // 4 digits always (pad as needed)
  float datum2_Float;     // 5 digits always (need pad) +1 for DP?
  char datum3_Char;       // 1 character always
  char datum4_String[3];  // 3 character always
  // might need constant for Comma or Return

// String to hold all data of one trial
  // ??? Size in bytes of total string
    // int            4
    // float         5  +1 for DP?
    // char         1
    // String[3]   3 plus +1 for terminating 0?
    // Commas  3
    // Return added to string or auto by SD append?
  char dataFullRecord[17]; //no Return character
  
// Get data from experiment - dummy values here
[indent]  datum1_Int = 12; // round/pad to 4 digits
  datum2_Float = 9.876543; // round/pad to 5 digits 
  datum3_Char = "A"; //
  datum4_String[4] = "June"; //always 4 char ?+1 for terminal 0
[/indent]

// Build string
    // how to concatenate?
    // how to round/pad int & float to fixed length?
    // how to convert int & float to string?
    // best way to include commas?
    // need Return character?
  // Attempt #6 error: doesn't know function strCopy 
    strCopy(dataFullRecord[](), cstr(round(datum1_int,3))
    strCopy(dataFullRecord[](), ",")
    strCopy(dataFullRecord[](), cstr(round(datum2_float,4))
    strCopy(dataFullRecord[](), ",")
    strCopy(dataFullRecord[](), datum3_char)
    strCopy(dataFullRecord[](), ",")
    strCopy(dataFullRecord[](), datum4_string[])
}

Comments

  • DavidZemonDavidZemon Posts: 2,973
    edited 2016-04-11 19:58
    You have a few different options for this. As always, my personal favorite is PropWare, where you'll have access to the Printer class for formatted printing. The Printer class is just a wrapper around any "PrintCapable" object, so you can use the same interface to print to the terminal, an SD card, an LCD interface, or others.

    An example using PropWare to do this would like a bit like this:
    // Some includes....
    
    using namespace PropWare;
    
    int main() {
        const SD  driver;
        FatFS     filesystem(driver);
        filesystem.mount();
    
        FatFileWriter writer(filesystem, "myfile.csv");
        writer.open();
        Printer filePrinter(writer, false); // The second parameter controls "cooked" mode. When true, \r is auto-inserted before any \n character
    
        // Vars to hold data for each trial until written
        int datum1_Int;         // 4 digits always (pad as needed)
        float datum2_Float;     // 5 digits always (need pad) +1 for DP?
        char datum3_Char;       // 1 character always
        char datum4_String[3];  // 3 character always
    
        // If datum4_String is null-terminated, like a standard C-string, you could use the %s instead of three %c
        filePrinter.printf("%04d,%05f,%c,%c%c%c\n", datum1_Int, datum2_Float, datum3_Char, datum4_String[0], datum4_String[1], datum4_String[2]);
    
        return 0;
    }
    

    But if you prefer to stick with what is already available in SimpleIDE and its included libraries, then your best bet is either sprint or fprintf. sprint is smaller since it is part of the "Simple" libraries, but its a 2-step process: create the string, write the string. fprintf will be much larger (because it is part of the C standard library) but easier, because you can write directly to the SD card.
  • sprintf() is probably the easiest to use. Have a fixed buffer that contains enough space to store the longest thing you'll ever need to convert to a string, then copy that buffer to your final output. Something like this:
    char tempBuf[16];  // plenty of space for a float or integer in ascii
    sprintf( tempBuffer, "%f", datum2_Float );
    
    strcpy( dataFullRecord, tempBuffer );
    

    Easier still would be to use sprintf to create the entire record:
    sprintf( dataFullRecord, "%04d,%f,%c", datum1_Int, datum2_Float, datum3_Char );
    

    Calling strlen() on the result would tell you the length of it.

    There is a function called itoa() which will convert from int to ascii, but there's no corresponding ftoa() for floats. For that you'd need to copy an implementation from online somwhere or roll your own. Often it's enough to do something like this:
    int integerPart = (int)myFloat;
    int fracPart = (int)((myFloat - (float)integerPart) * 1000.0f); // change the 1000 to be whatever scale you need
    

    This won't work if the float it outside of the displayable range of integers - floats can represent very large numbers - but for typical uses it works ok.
  • JasonDorie wrote: »
    sprintf() is probably the easiest to use. Have a fixed buffer that contains enough space to store the longest thing you'll ever need to convert to a string, then copy that buffer to your final output. Something like this:
    char tempBuf[16];  // plenty of space for a float or integer in ascii
    sprintf( tempBuffer, "%f", datum2_Float );
    
    strcpy( dataFullRecord, tempBuffer );
    

    Easier still would be to use sprintf to create the entire record:
    sprintf( dataFullRecord, "%04d,%f,%c", datum1_Int, datum2_Float, datum3_Char );
    

    Calling strlen() on the result would tell you the length of it.

    There is a function called itoa() which will convert from int to ascii, but there's no corresponding ftoa() for floats. For that you'd need to copy an implementation from online somwhere or roll your own. Often it's enough to do something like this:
    int integerPart = (int)myFloat;
    int fracPart = (int)((myFloat - (float)integerPart) * 1000.0f); // change the 1000 to be whatever scale you need
    

    This won't work if the float it outside of the displayable range of integers - floats can represent very large numbers - but for typical uses it works ok.

    Whoops! I don't know why I said "sscan" in my post :P fixing now..
  • I thought that was probably what you meant. :)
  • Thanks, guys. I started studying after first post and now have an introduction to sscan under my belt!

    I will switch to sprintf to study this evening as below. It looks like the conversion I was looking for is done easily by the formating codes in the second argument.

    To add another value named datum4_string which is a string[3], what would be formater code and syntax of additional argument?

    sprintf( dataFullRecord, "%04d,%f,%c", datum1_Int, datum2_Float, datum3_Char );


  • sprintf follows the same standard used by printf, dprintf, scanf, print, printi, sscan, sscani, System.out.printf (java), Printer.printf, etc, etc, etc.

    The number of formatting characters supported varies by implementation, but you're asking for pretty basic stuff here so you'd have access to these anywhere (accept for the special functions that end in "i" provided by Simple, such as printi and sscani). Check out this page for detailed descriptions and examples for the various formatting characters:

    http://www.cplusplus.com/reference/cstdio/printf/

    And now that you understand they all function the same, I'll show you again the example for PropWare's Printer.printf, which you can use for sprintf as well:
    filePrinter.printf("%04d,%05f,%c,%c%c%c\n", datum1_Int, datum2_Float, datum3_Char, datum4_String[0], datum4_String[1], datum4_String[2]);
    

    So to convert that to sprintf would be
    sprintf(buffer, "%04d,%05f,%c,%c%c%c\n", datum1_Int, datum2_Float, datum3_Char, datum4_String[0], datum4_String[1], datum4_String[2]);
    
  • DavidZemon wrote: »
    So to convert that to sprintf would be
    sprintf(buffer, "%04d,%05f,%c,%c%c%c\n", datum1_Int, datum2_Float, datum3_Char, datum4_String[0], datum4_String[1], datum4_String[2]);
    
    You should also be able to do this:
    int len = sprintf(buffer, "%04d,%05f,%c,%.3s\n", datum1_Int, datum2_Float, datum3_Char, datum4_String);
    
    Notice that sprintf() returns the number of bytes in the output string so there is no need to call strlen().
  • I do it a bit differently. Instead of making one string, I make a string for each value in a record and save them separated by commas. After the last value I save a "\n" to end the record. In excel I import with commas separating columns and new-line to switch to a new row.

    I did it this way because I had written the convert & write-to-file code for saving ping readings vs angle as I had a robot map a space. I then just copied the snippet for a temperature reading device, and then added a couple of devices that read temperature, pressure, and humidity. As I added data that I wanted to save, I just recopied the snippet.

    This is the code I use for saving the T, P, H data. ( I open & close the file after each record since I am taking reading every 10 minutes, and don't want to lose data if the batteries die.)

    The numbers I am converting are all floats with 2 decimal places, so I could have used a for() loop, but I have also used that method for chars and ints. If using non floats, I will usually use "sprinti" (with the Simple Libraries) since that saves about 3 to 4k bytes. In this case there was a lot of floating point math, so using the integer/char only formatting function wasn't worth it.

    atemp, apres, and ahum are all declared as chars with length of 12, e.g. char atemp[12];

    Tom
       FILE* fp = fopen(filen, "a");  
    
    // The next statements change a number to text and save it in a format excel can use
        len1 = sprint(atemp, "%.2f", T);            // convert float to string
        fwrite(&atemp, len1, 1, fp);                // Write degrees to SD card, first column of record
        fwrite(",", 1, 1, fp);                      // Write comma separates columns
    
        len2 = sprint(apres, "%.2f", P);    
        fwrite(&apres, len2, 1, fp);                // Write pressure to SD card column 2 of record
        fwrite(",", 1, 1, fp);                      // Write comma separates columns
    
        len3 = sprint(ahum, "%.2f", RHt);    
        fwrite(&ahum, len3, 1, fp);                 // Write humidity to SD card column 3 of record
        fwrite("\n", 1, 1, fp);                     // Write newline, next value will be in new row
        fclose(fp);                                 // Close file
    
  • Thanks for explanations. I read the page you linked, DaveZ, thanks. It said in the case of the %s specifier teh default is read to the terminating NULL unless a precision value was added. But I don't think that applies when using the %c%c%c as you suggested.

    3 out of 4 data look great when I print the buffer string named dataFullRecord.
    (One change I made for the variable datum3_char: Changing the assigned value from "A" to 65.)

    But the datum3_string (="Jun") always prints odd characters. Do I have to add the terminating zero to the string I am making?

    Here are central lines:
    // Create string and print
      sprintf(dataFullRecord , "%04d,%05f,%c,%c%c%c", 
        datum1_Int, datum2_Float, datum3_Char, 
        datum4_String[0], datum4_String[1], datum4_String[2]);
      print("%s\n",dataFullRecord); 
      // output result: 0012,9.876543,A, >   // last datum not correct
    

    Here is the full code:
    /*  StoreDataStringver2.
    Goal: Concatenate to 1 string values of differetn data types
    */
    #include "simpletools.h"                      // Include simple tools
    #include "simpletext.h"
    #include "simpletools.h"                      // Include simple tools
    int main()                                    // Main function
    {
    // Vars to hold data for each trial until written
      int datum1_Int;         // 4 digits always (pad as needed)
      float datum2_Float;     // 5 digits always (need pad) +1 for DP?
      char datum3_Char;       // 1 character always
      char datum4_String[3];  // 3 character always
    
    // String to hold all data of one trial
      char dataFullRecord[17]; //
      int stringLength;
    
    // Get data from experiment - dummy values here
      datum1_Int = 12; // round/pad to 4 digits
      datum2_Float = 9.876543; // round/pad to 5 digits 
      datum3_Char = 65; // = A. Doesn't work with "A"
      datum4_String[3] = "Jun"; //always 3 char ?+1 for terminal 0
    
    // Create string and print
      sprintf(dataFullRecord , "%04d,%05f,%c,%c%c%c", 
        datum1_Int, datum2_Float, datum3_Char, 
        datum4_String[0], datum4_String[1], datum4_String[2]);
      print("%s\n",dataFullRecord); 
      // output result: 0012,9.876543,A, >   // last datum not correct
    
    // alternate Create string and print
      sprintf(dataFullRecord , "%04d,%05f,%c,%0.3s", 
        datum1_Int, datum2_Float, datum3_Char, datum4_String);
      print("%s\n",dataFullRecord); 
      // output result: flash of text then a question mark 
    
    }
    
  • Instead of %c%c%c can you use %s

    Tom
  •   datum4_String[3] = "Jun"; //always 3 char ?+1 for terminal 0
    
    You can't assign a string like that. First, you'll have make the array four characters long to include space for the terminating zero and you'll have to use something like strcpy to initialize the array:
      char datum4_String[4];  // 3 character always plus one for the terminating zero
      strcpy(datum4_String, "Jun");
    
  • David Betz wrote: »
      datum4_String[3] = "Jun"; //always 3 char ?+1 for terminal 0
    
    You can't assign a string like that. First, you'll have make the array four characters long to include space for the terminating zero and you'll have to use something like strcpy to initialize the array:
      char datum4_String[4];  // 3 character always plus one for the terminating zero
      strcpy(datum4_String, "Jun");
    

    Is that a C thing? I init strings like that all the time - though I generally let the compiler determine the length for me
    const char myString[] = " Hello";
    
  • DavidZemon wrote: »
    David Betz wrote: »
      datum4_String[3] = "Jun"; //always 3 char ?+1 for terminal 0
    
    You can't assign a string like that. First, you'll have make the array four characters long to include space for the terminating zero and you'll have to use something like strcpy to initialize the array:
      char datum4_String[4];  // 3 character always plus one for the terminating zero
      strcpy(datum4_String, "Jun");
    

    Is that a C thing? I init strings like that all the time - though I generally let the compiler determine the length for me
    const char myString[] = " Hello";
    
    I think that only works when you're initializing strings. He is doing a separate assignment in his example code. Can you do that in C++?


  • ElectrodudeElectrodude Posts: 1,614
    edited 2016-04-12 03:32
    DavidZemon wrote: »
    David Betz wrote: »
      datum4_String[3] = "Jun"; //always 3 char ?+1 for terminal 0
    
    You can't assign a string like that. First, you'll have make the array four characters long to include space for the terminating zero and you'll have to use something like strcpy to initialize the array:
      char datum4_String[4];  // 3 character always plus one for the terminating zero
      strcpy(datum4_String, "Jun");
    

    Is that a C thing? I init strings like that all the time - though I generally let the compiler determine the length for me
    const char myString[] = " Hello";
    

    The difference is that in John Kauffman's code, he is declaring the variable and then assigning to it later, while your are declaring and initializing it at the same time. His code would have worked fine in C++, and yours would have worked fine in C - however, your way is definitely better because it lets the compiler figure out the length.
  • Works like a charm. Many thanks not only for guiding me to a solution but also for the logic along the way.
    Now it is on to appending the string to SD, once for each trial.
  • All works and prints. First tests to SD work.
    But why is string length less than observed?
    // Create string and print
      stringLength = sprintf(dataFullRecord , 
        "%4d,%3.2f,%c,%s", 
        datum1_Int, datum2_Float, datum3_Char, datum4_String);
      print("%s\nLength = %d\n\n\n",dataFullRecord,stringLength);
    

    The output is
    12,9.88,A,Jun
    Length = 11
    
    But I count length = 15, including the 2 leading spaces.
    In my first experiments writing to the card I only get the whole string saved and read by using stringLength+4
  • It is not shown in code format in above message that actually on terminal there are 2 spaces before the string above - they don't seem to copy paste.
    " 12.9.88,A,Jun"
  • It may be a bug in the implementation. Using strlen() on the result will likely be accurate.
  • JasonDorie wrote: »
    It may be a bug in the implementation. Using strlen() on the result will likely be accurate.
    Ugh. Yes, that does look like a bug.

  • TorTor Posts: 2,010
    Just for completeness, in C you can assign a char pointer to a fixed string, and later freely move it to point to another string, like so:
        char *a;
        char *b;
    
        a = "Hey";
        b = "World";
        printf ("%s %s\n", a, b);
    
        a = "No";
        b = "Way";
        printf ("%s %s\n", a, b);
    
    There's no copying of strings going on there, just moving pointers around. a and b are pointers, so what changes is the value they hold, which is an address in this case.
  • "John wrote:
    But I count length = 15, including the 2 leading spaces.

    That does sound like a bug, although it's not there in the most recent PropGCC, so it's been fixed since the SimpleIDE release (which was quite some time ago :( ).

    As Jason suggested, strlen() on the string will work.
  • David BetzDavid Betz Posts: 14,511
    edited 2016-04-12 17:15
    Eric is correct. Here is the program I tested under a current build of PropGCC:
    #include <stdio.h>
    
    void main(void)
    {
      char dataFullRecord[100];
      int datum1_Int = 12;
      float datum2_Float = 9.88;
      char datum3_Char = 'A';
      char datum4_String[4] = "Jun";
      int stringLength;
    
    // Create string and print
      stringLength = sprintf(dataFullRecord , 
        "%4d,%3.2f,%c,%s", 
        datum1_Int, datum2_Float, datum3_Char, datum4_String);
      printf("%s\nLength = %d\n\n\n",dataFullRecord,stringLength);
    }
    
    And here is the output:
      12,9.88,A,Jun
    Length = 15
    
Sign In or Register to comment.