数値を文字列に変換する – mylib | C言語

atoiやatofの逆で、数値を文字列に変換する関数を考えます。

目次

printf系関数は重く、遅い

C言語には、数値をだいたい狙った通りのフォーマットで文字列に変換してくれる、sprintf関数というものがあります。しかしこのsprintf関数、多機能であるがゆえにとても多くのプログラムメモリーを消費し、かつ変換速度も遅いです。このため組み込みソフトでは使いづらい面もあります。

そこで数値を文字列に変換する関数をまとめた、「stdlib.h」ならぬ「mylib.h」を考えてみました。

目標

  • 符号あり整数を文字列に変換します。
  • 符号なし整数を文字列に変換します。
  • 浮動小数点数を簡単な小数点付き文字列に変換します。
  • 小数点の記号にピリオドだけでなく、コンマも選択できます。
  • 文字列の最後には’\0’が付きます。
  • 変換条件を満たしていない場合は、エラーを返すと同時に先頭を’\0’にします。

サンプルコード

それぞれの関数の変換例は関数ヘッダーにコメントとして記載しているので、それを参考にどうぞ。その他では下記のような機能があります。

[mylib.h]

#ifndef MYLIB_H
#define MYLIB_H

#ifdef __cplusplus
extern "C" {
#endif

//------------------------------------------------------------------------------
// include
//------------------------------------------------------------------------------
#include <stdbool.h>
#include <stdint.h>

//------------------------------------------------------------------------------
// define
//------------------------------------------------------------------------------
// 戻り値
#define OUT_OF_RANGE    (1)  //!< 引数が範囲外
#define BUFFER_OVERFLOW (2)  //!< バッファー不足

//------------------------------------------------------------------------------
// function
//------------------------------------------------------------------------------
void Set_DecimapSeparator(uint8_t separator);

uint8_t UIntToAscii(uint32_t value, char ascii[], uint8_t len, char padding);
uint8_t IntToAscii(int32_t value, char ascii[], uint8_t len, char padding);
uint8_t IntFloatToAscii(int32_t value, char ascii[], uint8_t len, uint8_t dec_place, char padding);
uint8_t DoubleToAscii(double value, char ascii[], uint8_t len, uint8_t dec_place, char padding);

#ifdef __cplusplus
}
#endif

#endif

[mylib.c]

/**
 * @file mylib.c
 * @brief マイライブラリー処理
 */

//------------------------------------------------------------------------------
// include
//------------------------------------------------------------------------------
#include "mylib.h"

//------------------------------------------------------------------------------
// macro
//------------------------------------------------------------------------------
#define POW_MAX (7)

//------------------------------------------------------------------------------
// private variable
//------------------------------------------------------------------------------
// 小数点の記号
static uint8_t m_decimap_separator = '.';

// べき乗
static const uint32_t m_pow10[POW_MAX] = {1, 10, 100, 1000, 10000, 100000, 1000000};

//------------------------------------------------------------------------------
// private function
//------------------------------------------------------------------------------
static uint8_t uitoa(uint32_t value, uint8_t *adr, char ascii[], uint8_t len);
static uint8_t Set_Padding(uint8_t adr, bool f_minus, char ascii[], uint8_t padding);

/** ----------------------------------------------------------------------------
 * @brief 小数点の記号を設定
 *
 * @param separator : 小数点の記号 [0=. / 1=,]
 */
void Set_DecimapSeparator(uint8_t separator)
{
  if (separator == 0) {
    m_decimap_separator = '.';
  } else {
    m_decimap_separator = ',';
  }
}

/** ----------------------------------------------------------------------------
 * @brief 符号なし整数を文字列に変換
 * @details (1234, ascii,  6, ' ') => ascii = "  1234"
 * @details (1234, ascii,  6, '0') => ascii = "001234"
 *
 * @param value : 符号なし整数
 * @param [out] ascii : 文字列
 * @param len : 文字列長
 * @param padding : パディング
 * @retval 0 : OK
 * @retval BUFFER_OVERFLOW : ascii
 */
uint8_t UIntToAscii(uint32_t value, char ascii[], uint8_t len, char padding)
{
  uint8_t ret;
  uint8_t adr;

  ret = uitoa(value, &adr, ascii, len);
  if (ret != 0) {
    ascii[0] = '\0';
    return ret;
  }

  ret = Set_Padding(adr, false, ascii, padding);
  if (ret != 0) {
    ascii[0] = '\0';
    return ret;
  }

  ascii[len] = '\0';

  return 0;
}
/** ----------------------------------------------------------------------------
 * @brief 符号あり整数を文字列に変換
 * @details ( 1234, ascii,  6, ' ') => ascii = "  1234"
 * @details (-1234, ascii,  6, ' ') => ascii = " -1234"
 * @details (-1234, ascii,  6, '0') => ascii = "-01234"
 *
 * @param value : 符号あり整数 [-INT32_MAX~INT32_MAX]
 * @param [out] ascii : 文字列
 * @param len : 文字列長
 * @param padding : パディング
 * @retval 0 : OK
 * @retval OUT_OF_RANGE : ascii < -INT32_MAX
 * @retval BUFFER_OVERFLOW : ascii
 */
uint8_t IntToAscii(int32_t value, char ascii[], uint8_t len, char padding)
{
  bool     f_minus = false;
  uint8_t  ret;
  uint8_t  adr;
  uint32_t uvalue;

  if (value < 0) {
    if (value < -INT32_MAX) {
      ascii[0] = '\0';
      return OUT_OF_RANGE;
    }
    f_minus = true;
    value *= -1;
  }
  uvalue = value;

  ret = uitoa(uvalue, &adr, ascii, len);
  if (ret != 0) {
    ascii[0] = '\0';
    return ret;
  }

  ret = Set_Padding(adr, f_minus, ascii, padding);
  if (ret != 0) {
    ascii[0] = '\0';
    return ret;
  }

  ascii[len] = '\0';

  return 0;
}

/** ----------------------------------------------------------------------------
 * @brief 小数を文字列に変換
 * @details ( 1234, ascii,  6, 2, ' ') => ascii = " 12.34"
 * @details (-1234, ascii,  6, 2, ' ') => ascii = "-12.34"
 * @details (-1234, ascii, 10, 5, ' ') => ascii = "  -0.01234"
 * @details (-1234, ascii, 10, 5, '0') => ascii = "-000.01234"
 *
 * @param value : 符号あり整数 [-INT32_MAX~INT32_MAX]
 * @param [out] ascii : 文字列
 * @param len : 文字列長
 * @param dec_place : 小数点以下の桁数
 * @param padding : パディング
 * @retval 0 : OK
 * @retval OUT_OF_RANGE : ascii < -INT32_MAX
 * @retval BUFFER_OVERFLOW : ascii
 */
uint8_t IntFloatToAscii(int32_t value, char ascii[], uint8_t len, uint8_t dec_place, char padding)
{
  bool     f_minus = false;
  uint8_t  ret;
  uint8_t  adr = len;
  uint32_t uvalue;
  uint32_t tmp;

  if ((dec_place > 0) && (len < dec_place + 2)) {
    ascii[0] = '\0';
    return BUFFER_OVERFLOW;
  }

  if (value < 0) {
    if (value < -INT32_MAX) {
      ascii[0] = '\0';
      return OUT_OF_RANGE;
    }
    f_minus = true;
    value *= -1;
  }
  uvalue = value;

  // 小数部
  if (dec_place > 0) {
    while (dec_place > 0) {
      --dec_place;
      tmp = uvalue;
      uvalue /= 10;
      ascii[adr - 1] = (char)((uint8_t)(tmp - uvalue * 10) | 0x30);
      --adr;
      if (uvalue == 0) {
        break;
      }
    }
    while (dec_place > 0) {
      --dec_place;
      ascii[adr - 1] = '0';
      --adr;
    }

    ascii[adr - 1] = m_decimap_separator;
    --adr;
  }

  // 整数部
  ret = uitoa(uvalue, &adr, ascii, adr);
  if (ret != 0) {
    ascii[0] = '\0';
    return ret;
  }

  ret = Set_Padding(adr, f_minus, ascii, padding);
  if (ret != 0) {
    ascii[0] = '\0';
    return ret;
  }

  ascii[len] = '\0';
  return 0;
}

/** ----------------------------------------------------------------------------
 * @brief 小数を文字列に変換
 * @details ( 12.34, ascii,  6, 2, ' ') => ascii = " 12.34"
 * @details (-12.34, ascii,  6, 2, ' ') => ascii = "-12.34"
 * @details (-0.01234, ascii, 10, 5, ' ') => ascii = "  -0.01234"
 * @details (-0.01234, ascii, 10, 5, '0') => ascii = "-000.01234"
 *
 * @param value : 符号あり整数
 * @param [out] ascii : 文字列
 * @param len : 文字列長
 * @param dec_place : 小数点以下の桁数
 * @param padding : パディング
 * @retval 0 : OK
 * @retval OUT_OF_RANGE : dec_place > POW_MAX - 1
 * @retval BUFFER_OVERFLOW : ascii
 */
uint8_t DoubleToAscii(double value, char ascii[], uint8_t len, uint8_t dec_place, char padding)
{
  uint8_t ret;

  if (dec_place > (POW_MAX - 1)) {
    ascii[0] = '\0';
    return OUT_OF_RANGE;
  }

  value = value * m_pow10[dec_place];
  if (value >= 0) {
    value += 0.5;
  } else {
    value -= 0.5;
  }

  ret = IntFloatToAscii((int32_t)value, ascii, len, dec_place, padding);
  return ret;
}

/** ----------------------------------------------------------------------------
 * @brief 符号なし整数を文字列に変換
 *
 * @param value : 符号なし整数
 * @param [out] adr : 変換後先頭アドレス
 * @param [out] ascii : 文字列
 * @param len : 文字列長
 * @retval 0 : OK
 * @retval BUFFER_OVERFLOW : ascii
 */
static uint8_t uitoa(uint32_t value, uint8_t *adr, char ascii[], uint8_t len)
{
  uint8_t  i;
  uint32_t tmp;

  for (i = len; i > 0; --i) {
    tmp = value;
    value /= 10;
    ascii[i - 1] = (char)((uint8_t)(tmp - value * 10) | 0x30);
    if (value == 0) {
      --i;
      break;
    }
  }
  if (value != 0) {
    return BUFFER_OVERFLOW;
  }

  *adr = i;
  return 0;
}

/** ----------------------------------------------------------------------------
 * @brief パディングを設定
 *
 * @param adr : パディングの挿入開始アドレス
 * @param f_minus : 負の値?
 * @param [out] ascii : 文字列
 * @param padding : パディング
 * @retval 0 : OK
 * @retval BUFFER_OVERFLOW : ascii
 */
static uint8_t Set_Padding(uint8_t adr, bool f_minus, char ascii[], uint8_t padding)
{
  if (f_minus == true) {
    if (adr == 0) {
      return BUFFER_OVERFLOW;
    }
    if (padding != '0') {
      f_minus        = false;
      ascii[adr - 1] = '-';
      --adr;
    }
  }

  while (adr > 0) {
    ascii[adr - 1] = padding;
    --adr;
  }

  if (f_minus == true) {
    ascii[0] = '-';
  }

  return 0;
}

サンプルコードの使用例

使用例代わりに、CppUTestによるテストコードを掲載しておきます。

#include "CppUTest/TestHarness.h"

#include "mylib.h"

// clang-format off
TEST_GROUP(mylib){
  TEST_SETUP(){
  }

  TEST_TEARDOWN(){
  }
};
// clang-format on

TEST(mylib, Test_UIntToAscii)
{
  char    ascii[15];
  uint8_t ret;

  // 成功パターン
  ret = UIntToAscii(1234, ascii, 4, ' ');
  UNSIGNED_LONGS_EQUAL(ret, 0);
  STRNCMP_EQUAL(ascii, "1234", 4);

  ret = UIntToAscii(1234, ascii, 6, ' ');
  UNSIGNED_LONGS_EQUAL(ret, 0);
  STRNCMP_EQUAL(ascii, "  1234", 6);

  ret = UIntToAscii(1234, ascii, 6, '0');
  UNSIGNED_LONGS_EQUAL(ret, 0);
  STRNCMP_EQUAL(ascii, "001234", 6);

  // 失敗パターン
  ret = UIntToAscii(1234, ascii, 3, ' ');
  UNSIGNED_LONGS_EQUAL(ret, BUFFER_OVERFLOW);
}

TEST(mylib, Test_IntToAscii)
{
  char    ascii[15];
  uint8_t ret;

  // 成功パターン
  ret = IntToAscii(1234, ascii, 4, ' ');
  UNSIGNED_LONGS_EQUAL(ret, 0);
  STRNCMP_EQUAL(ascii, "1234", 4);

  ret = IntToAscii(1234, ascii, 6, ' ');
  UNSIGNED_LONGS_EQUAL(ret, 0);
  STRNCMP_EQUAL(ascii, "  1234", 6);

  ret = IntToAscii(1234, ascii, 6, '0');
  UNSIGNED_LONGS_EQUAL(ret, 0);
  STRNCMP_EQUAL(ascii, "001234", 6);

  ret = IntToAscii(-1234, ascii, 5, ' ');
  UNSIGNED_LONGS_EQUAL(ret, 0);
  STRNCMP_EQUAL(ascii, "-1234", 5);

  ret = IntToAscii(-1234, ascii, 6, ' ');
  UNSIGNED_LONGS_EQUAL(ret, 0);
  STRNCMP_EQUAL(ascii, " -1234", 6);

  ret = IntToAscii(-1234, ascii, 6, '0');
  UNSIGNED_LONGS_EQUAL(ret, 0);
  STRNCMP_EQUAL(ascii, "-01234", 6);

  // 失敗パターン
  ret = IntToAscii(1234, ascii, 3, ' ');
  UNSIGNED_LONGS_EQUAL(ret, BUFFER_OVERFLOW);

  ret = IntToAscii(-2147483648, ascii, 11, ' ');
  UNSIGNED_LONGS_EQUAL(ret, OUT_OF_RANGE);
}

TEST(mylib, Test_IntFloatToAscii)
{
  char    ascii[15];
  uint8_t ret;

  // 成功パターン
  ret = IntFloatToAscii(12345, ascii, 7, 1, ' ');
  UNSIGNED_LONGS_EQUAL(ret, 0);
  STRNCMP_EQUAL(ascii, " 1234.5", 7);

  ret = IntFloatToAscii(12345, ascii, 7, 2, ' ');
  UNSIGNED_LONGS_EQUAL(ret, 0);
  STRNCMP_EQUAL(ascii, " 123.45", 7);

  ret = IntFloatToAscii(12345, ascii, 7, 5, ' ');
  UNSIGNED_LONGS_EQUAL(ret, 0);
  STRNCMP_EQUAL(ascii, "0.12345", 7);

  Set_DecimapSeparator(1);  // 小数点の記号をコンマに設定

  ret = IntFloatToAscii(-12345, ascii, 8, 4, ' ');
  UNSIGNED_LONGS_EQUAL(ret, 0);
  STRNCMP_EQUAL(ascii, " -1,2345", 8);

  Set_DecimapSeparator(0);  // 小数点の記号をピリオドに設定

  ret = IntFloatToAscii(-12345, ascii, 8, 4, '0');
  UNSIGNED_LONGS_EQUAL(ret, 0);
  STRNCMP_EQUAL(ascii, "-01.2345", 8);

  // 失敗パターン
  ret = IntFloatToAscii(12345, ascii, 5, 1, ' ');
  UNSIGNED_LONGS_EQUAL(ret, BUFFER_OVERFLOW);

  ret = IntFloatToAscii(-2147483648, ascii, 12, 3, ' ');
  UNSIGNED_LONGS_EQUAL(ret, OUT_OF_RANGE);
}

TEST(mylib, Test_DoubleToAscii)
{
  char    ascii[15];
  uint8_t ret;

  // 成功パターン
  ret = DoubleToAscii(1234.5, ascii, 7, 1, ' ');
  UNSIGNED_LONGS_EQUAL(ret, 0);
  STRNCMP_EQUAL(ascii, " 1234.5", 7);

  Set_DecimapSeparator(1);  // 小数点の記号をコンマに設定

  ret = DoubleToAscii(123.45, ascii, 7, 2, ' ');
  UNSIGNED_LONGS_EQUAL(ret, 0);
  STRNCMP_EQUAL(ascii, " 123,45", 7);

  ret = DoubleToAscii(123.45, ascii, 6, 1, ' ');
  UNSIGNED_LONGS_EQUAL(ret, 0);
  STRNCMP_EQUAL(ascii, " 123,5", 6);

  Set_DecimapSeparator(0);  // 小数点の記号をピリオドに設定

  ret = DoubleToAscii(0.12345, ascii, 7, 5, ' ');
  UNSIGNED_LONGS_EQUAL(ret, 0);
  STRNCMP_EQUAL(ascii, "0.12345", 7);

  ret = DoubleToAscii(0.12345, ascii, 8, 6, ' ');
  UNSIGNED_LONGS_EQUAL(ret, 0);
  STRNCMP_EQUAL(ascii, "0.123450", 8);

  ret = DoubleToAscii(-1.2345, ascii, 8, 4, ' ');
  UNSIGNED_LONGS_EQUAL(ret, 0);
  STRNCMP_EQUAL(ascii, " -1.2345", 8);

  ret = DoubleToAscii(-1.2345, ascii, 8, 4, '0');
  UNSIGNED_LONGS_EQUAL(ret, 0);
  STRNCMP_EQUAL(ascii, "-01.2345", 8);

  ret = DoubleToAscii(-1.2345, ascii, 7, 3, ' ');
  UNSIGNED_LONGS_EQUAL(ret, 0);
  STRNCMP_EQUAL(ascii, " -1.235", 7);

  ret = DoubleToAscii(-1.2345, ascii, 6, 2, ' ');
  UNSIGNED_LONGS_EQUAL(ret, 0);
  STRNCMP_EQUAL(ascii, " -1.23", 6);

  // 失敗パターン
  ret = DoubleToAscii(1234.5, ascii, 5, 1, ' ');
  UNSIGNED_LONGS_EQUAL(ret, BUFFER_OVERFLOW);

  ret = DoubleToAscii(1.23456789, ascii, 10, 8, ' ');
  UNSIGNED_LONGS_EQUAL(ret, OUT_OF_RANGE);

  ret = DoubleToAscii(-2147483.648, ascii, 12, 3, ' ');
  UNSIGNED_LONGS_EQUAL(ret, OUT_OF_RANGE);
}
目次