A way to format string on Arduino(Wio Terminal)
Arduino ではString::format
が使えない
大抵のプログラミング言語では、フォーマット文字列と変数を用いて、動的に文字列を生成することができます。
C++の場合
関数std::format()
を使います。
#include <iostream>
#include <format>
int main() {
int iValue = 42;
float fValue = 3.14159f;
double dValue = 2.718281828459;
std::string formatted = std::format("Integer: {}, Float: {}, Double: {:.4f}", iValue, fValue, dValue);
std::cout << formatted << std::endl;
std::cout << __cplusplus << std::endl; // C++のバージョンが表示される
return 0;
}
ただしstd::format
はC++20
の機能であるため、それよりも古い方式でコンパイルするとerror: 'format' is not a member of 'std'
というエラーになります。その際は、次のようにC++20
を指定する必要があります。
g++ -std=c++20 .\main.cpp # ここではmain.cppというファイル名
実行結果はこの通りです。
Integer: 42, Float: 3.14159, Double: 2.7183
202002
Pythonの場合
文字列の頭にf
を付け、f'~'
とするだけで可能です。
>>> iValue: int = 42
>>> fValue: float = 2.718281828459
>>> dic: dict = {'date': '2025-06-01', 'title': 'dictionary', 'height': 24}
>>> f'int: {iValue}, float: {fValue}, dict: {dic}'
"int: 42, float: 2.718281828459, dict: {'date': '2025-06-01', 'title': 'dictionary', 'height': 24}"
>>> f'{fValue:.2f}'
'2.72'
>>> f'{fValue:.2}'
'2.7'
>>> f'{fValue:.9f}'
'2.718281828'
>>> f'{fValue:.9}'
'2.71828183'
関数format()
もありますが、やや癖があります。
>>> "{}".format(2)
'2'
Rustの場合
マクロformat!()
を使います。
fn main() {
let u64_value: u64 = 42;
let f64_value: f64 = 2.718281828459;
let ai64_array: [i64; 5] = [1, 3, 5, 7, 9];
// 配列のformatには{:?}を使う
let s_format = format!("uint: {}, float: {}, array: {:?}", u64_value, f64_value, ai64_array);
println!("{s_format}");
}
uint: 42, float: 2.718281828459, array: [1, 3, 5, 7, 9]
一方現状のformat
に近しい機能が使えないようです。従って、sprintf
ではなく、比較的安全とされるsnprintf
を使いましょう。
#include <stdio.h>
int main() {
const int iBufferSize = 64;
char acBuffer[iBufferSize];
int iValue = 42;
float fValue = 3.14159f;
double dValue = 2.718281828459;
snprintf(acBuffer, iBufferSize, "Integer: %d, Float: %f, Double: %.4f", iValue, fValue, dValue);
printf("%s", acBuffer);
return 0;
}
Integer: 42, Float: 3.141590, Double: 2.7183
なおconst int iBufferSize = 16;
と変更すると、表示がInteger: 42, Fl
で途切れます。sprintf
で同じことを真似するとエラーになります。
AI に実装させた
文字列を扱うたびに専用の配列とバッファーサイズを管理するのが忌まわしくて仕方ないので、format
を実装させました。
AIによる実装例
#include <type_traits> // std::is_same
// ベースケース: 引数がない場合
String format_impl(const String& format_str) {
return format_str;
}
// 浮動小数点数を指定した小数点以下桁数で文字列化する関数
String floatToString(float value, int digits) {
char buf[32];
dtostrf(value, 0, digits, buf); // dtostrf(値, 全体幅, 小数点以下桁数, バッファ)
return String(buf);
}
// 再帰的なフォーマット関数
template<typename T, typename... Args>
String format_impl(const String& format_str, T first_arg, Args... rest_args) {
// フォーマット文字列内の浮動小数点数のプレースホルダー "{:.nf}" を探す
int float_placeholder_pos = format_str.indexOf("{:."); // 例: {:.4f}
// 浮動小数点数のプレースホルダーが見つかった場合
if (float_placeholder_pos != -1) {
int end = format_str.indexOf("f}", float_placeholder_pos);
if (end != -1) {
String before = format_str.substring(0, float_placeholder_pos);
String after = format_str.substring(end + 2);
// first_argがfloat/double型の場合のみfloatToStringを使う
if constexpr (std::is_same<T, float>::value || std::is_same<T, double>::value) {
int digits = format_str.substring(float_placeholder_pos + 3, end).toInt();
return before + floatToString(first_arg, digits) + format_impl(after, rest_args...);
} else {
return before + String(first_arg) + format_impl(after, rest_args...);
}
}
}
// 通常のプレースホルダー "{}" を探す
int placeholder_pos = format_str.indexOf("{}");
// プレースホルダーが見つからない場合
if (placeholder_pos == -1) {
return format_str;
}
String before_placeholder = format_str.substring(0, placeholder_pos);
String after_placeholder = format_str.substring(placeholder_pos + 2); // "{}" は2文字
return before_placeholder + String(first_arg) + format_impl(after_placeholder, rest_args...);
}
// ユーザー向けのエントリポイント
template<typename... Args>
String format(const char* fmt_cstr, Args... args) {
return format_impl(String(fmt_cstr), args...);
}
void setup() {
Serial.begin(115200);
while (!Serial);
int p1 = 10;
float p2 = 1.5;
String s1 = format("param1: {}, param2: {}", p1, p2);
Serial.println(s1);
String s2 = format("Hello, {}!", "World");
Serial.println(s2);
String s3 = format("Number: {}", 42);
Serial.println(s3);
String s4 = format("Number: {}", 45.6);
Serial.println(s4);
String s5 = format("Number: {:.1f}", 45.6);
Serial.println(s5);
String s6 = format("Napier: {:.5f}", 2.71828);
Serial.println(s6);
// 引数が足りない場合
String sShortage = format("One param: {}, two param: {}", 100);
Serial.println(sShortage);
// 引数が多い場合
String sExcess = format("Just one: {}", 1, 2);
Serial.println(sExcess);
}
void loop() {}
シリアルモニターへの出力結果はこの通りです。
param1: 10, param2: 1.50
Hello, World!
Number: 42
Number: 45.60
Number: 45.6
Napier: 2.71828
One param: 100, two param: {}
Just one: 1
String::format
が使える
本題:実は表題にある通り「String::format
が使えない」と申しますが、「実はString::format
を使えます」。このように矛盾した情報が混在するのは、String
の定義が異なるためです。
Arduino (AVR )の場合
俗にString
クラスは、当然std::string
とも異なります。またシリアルモニターで頻繁に使うSerial
は、厳密にはSerial_
クラスから生成されたオブジェクト(インスタンス)です。
String
クラスの定義はこのように存在します。
これを見ると、format
という定義は見当たらないことが確認できます。従って、「String::format
が使えない」のです。
Seeeduino (SAMD )の場合
一方、
String
クラスの定義はこのようにあります。
これを見ると、String::format
の定義が認められます。
この実装はソースファイルに分かれています。
従って「String::format
が使える」のです。
void setup() {
Serial.begin(115200);
while (!Serial);
Serial.println( String::format("Hello, %s!", "World") );
Serial.println( String::format("Number: %d", 42) );
Serial.println( String::format("Number: %f", 45.6) );
Serial.println( String::format("Napier: %.5f", 2.71828182846) );
Serial.println( String::format("One param: %d, two param: %d", 100) );
Serial.println( String::format("Just one: %d", 1, 2) );
}
void loop() {}
Hello, World!
Number: 42
Number: 45.600000
Napier: 2.71828
One param: 100, two param: 536871044
Just one: 1
但し、フォーマット指定子に対して引数が足らない場合、コンパイルエラーなどにはなりません。何らかの予期せぬ値が出てくるようです。危険ですね。
まとめ
String::format
の実装を移植すれば、
或いは%d
、小数は%f
、文字列は%s
など)が必要なく、{}
で統一されている点も親切です。
String::format 実装移植 |
|
|
---|---|---|
プレースホルダー | 適切なフォーマット指定子 | 一律{}
|
引数不足時 | 未定義? |
{} がそのまま残る |
実装コード |
|
<type_traits>
|
Discussion