C言語教室 余談1 - 数字は何桁ごとに区切るのか
大きな数字を表現するときに、3桁毎にカンマ記号で区切って表示するというのが特にアメリカでは多いのですが、この表現が実は国によって異なるので、多くのシステムでは locale という仕組みで実装されています。ただ、この locale、結構複雑で自分が知らない習慣に対して、何が正しいのかがよくわからないので、テストをするにも苦労します。また環境によって思った通りの動作をするのかが微妙なので、トラブルになることも多く、いっそのこと自力で処理をしてしまえということも良くあります。
いつも答案を頂く Akio van der Meer さんが、この3桁区切りに挑戦されたようで、興味深く拝見させて頂きました(後ろの方にある追記部分にあります)。
(答案提出) C言語教室 第14回 - 変数型のサイズ
基本的には標準ライブラリにある itoa を自作すればいいんです。基数は10固定で良いと思うのですが、負の数があるよなと思いながら書いてみます。先にバッファを用意してバッファのオケツから結果を放り込めば良さそうなんですが、脳みそがうまく逆にならなかったので、最後にひっくり返すことにしました。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define INTMAX_STRING_LENGTH 16
void swap(char *p, char *q) {
char t = *p;
*p = *q;
*q = t;
}
char* reverse_string(char *buf) {
int l_pos = 0;
int r_pos = strlen(buf)-1;
while (l_pos < r_pos)
swap(&buf[l_pos++], &buf[r_pos--]);
return buf;
}
char* thousands_separator_numeric(int v) {
char buf[INTMAX_STRING_LENGTH];
int n = abs(v);
int i = 0;
int j = 0;
int r;
while (n) {
r = n % 10;
buf[i++] = '0' + r;
n = n / 10;
if ((j++ % 3 == 2) && (n > 0))
buf[i++] = ',';
}
if (i == 0)
buf[i++] = '0';
if (v < 0)
buf[i++] = '-';
buf[i++] = '\0';
reverse_string(buf);
return strcpy((char*)malloc(strlen(buf)+1), buf);
}
void main() {
int origin[] = {
100000000,
-10000000,
2147483647,
-2147483647,
0
};
int i;
char *fomatted;
for (i = 0; i < 5; i++) {
fomatted = thousands_separator_numeric(origin[i]);
printf("value:%d->%s\n", origin[i], fomatted);
free(fomatted);
}
}
バッファの大きさは、limits.h をインクルードして INT_MIN に対して何文字必要なのかを計算すれば良いのですが、これを動的にやるのも何なんで、ちゃんと計算したいのであれば、configure で設定できるようにしてもらうこととして固定的な値に定義してしまいました。さて、これで大丈夫かな?
value:100000000->100,000,000
value:-10000000->-10,000,000
value:2147483647->2,147,483,647
value:-2147483647->-2,147,483,647
value:0->0
実は0がちゃんと0で出てくるかが大事だったりします。結果は大丈夫のようですが、thousands_separator_numeric の最後で、文字列を反転させてからコピーしているのが気に入りません。ローカル変数へのポインタを返すわけにはいかないのでコピーしたのですが、コピーするくらいならコピーしながら反転させれば1度で済むはずです。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define INTMAX_STRING_LENGTH 16
char* string_reverse_copy(char *s) {
unsigned int len = strlen(s);
char *d = (char*)malloc(len + 1);
if (d != NULL) {
char *p = s + len;
char *q = d;
while (p-- != s)
*q++ = *p;
*(d + len) = '\0';
}
return d;
}
char* thousands_separator_numeric(int v) {
char buf[INTMAX_STRING_LENGTH];
int n = abs(v);
int i = 0;
int j = 0;
int r;
while (n) {
r = n % 10;
buf[i++] = '0' + r;
n = n / 10;
if ((j++ % 3 == 2) && (n > 0))
buf[i++] = ',';
}
if (i == 0)
buf[i++] = '0';
if (v < 0)
buf[i++] = '-';
buf[i++] = '\0';
return string_reverse_copy(buf);
}
void main() {
int origin[] = {
100000000,
-10000000,
2147483647,
-2147483647,
0
};
int i;
char *fomatted;
for (i = 0; i < 5; i++) {
fomatted = thousands_separator_numeric(origin[i]);
printf("value:%d->%s\n", origin[i], fomatted);
free(fomatted);
}
}
反転させながらのコピーって、ループ終了の判定が慣れていません。少し試行錯誤してしまいました。これでも良いとは思うのですが、そもそも最初から逆順に文字列を生成していけばコピーなんていらないはずです。初心に帰って最後の文字から文字列を生成する方法を考えます。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define INTMAX_STRING_LENGTH 16
char* thousands_separator_numeric(int v, char *buf, int len) {
int n = abs(v);
int i = len - 1;
int j = 0;
int r;
buf[i--] = '\0';
while (n) {
r = n % 10;
buf[i--] = '0' + r;
n = n / 10;
if ((j++ % 3 == 2) && (n > 0))
buf[i--] = ',';
}
i++;
if (i == (len - 1))
buf[--i] = '0';
if (v < 0)
buf[--i] = '-';
return &buf[i];
}
void main() {
int origin[] = {
100000000,
-10000000,
2147483647,
-2147483647,
0
};
int i;
char buf[INTMAX_STRING_LENGTH];
char *formatted;
for (i = 0; i < 5; i++) {
formatted = thousands_separator_numeric(origin[i], buf, INTMAX_STRING_LENGTH);
printf("value:%d->%s\n", origin[i], formatted);
}
}
ローカル変数を使うと結局コピーすることになるので、ここは呼び出し側で領域を確保してしまうことにしてしまいました。ただ渡したバッファの途中のアドレスが返るという使い方はちょっと気持ち悪いといえば気持ち悪いかもしれません。ヨシヨシ、これでカンマを挿入できたかな。
もっと大きなサイズの整数型への変更は容易だと思いますが、実数は、いろいろな確認が増えるので大変そうですね(書式もいろいろありますし)。このコードを応用すれば、カンマではなく空白を入れるとかも書けそうです。出力を漢数字にしてみるのも面白いかも。自分でコードを用意するといろいろカスタムするのが簡単ですからね。
ヘッダ画像は、いらすとや さんより
https://www.irasutoya.com/2016/03/blog-post_516.html