はじめに
1 から 100000000 以下の任意の値までの FizzBuzz を出力します。
実行結果
$ ./fizzbuzz 20
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
16
17
Fizz
19
Buzz
$ ./fizzbuzz 123|tail
Fizz
Buzz
116
Fizz
118
119
FizzBuzz
121
122
Fizz
$ ./fizzbuzz 1234|tail
Buzz
1226
Fizz
1228
1229
FizzBuzz
1231
1232
Fizz
1234
$ ./fizzbuzz 10000|tail
9991
9992
Fizz
9994
Buzz
Fizz
9997
9998
Fizz
Buzz
$ ./fizzbuzz 100000000|tail
99999991
99999992
Fizz
99999994
Buzz
Fizz
99999997
99999998
Fizz
Buzz
$
以上のような動作をするプログラムとなっています。
コードをご紹介
MAX = 100000000
target: fizzbuzz
fizzbuzz: fizzbuzz.c fizzbuzz.txt
gcc -Wall -Wextra -O2 -DMax=$(MAX) fizzbuzz.c -o $@ -Wl,-Map=$@.map
fizzbuzz.txt: makefile
perl -e 'for($$i=1;$$i<=$(MAX);$$i++){print"Fizz"if$$i%3==0;print"Buzz"if$$i%5==0;print$$i if$$i%3&&$$i%5;print"\n"}' > $@
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
static inline size_t _computeSizeOfFizzBuzz(int start, int end, int numOfDigits)
{
size_t size = 0;
for (int i = start; i <= end; i++) {
_Bool f = i % 3 == 0;
_Bool b = i % 5 == 0;
if (f) {
size += strlen("Fizz");
}
if (b) {
size += strlen("Buzz");
}
if (!f && !b) {
size += numOfDigits;
}
size += strlen("\n");
}
return size;
}
size_t computeSizeOfFizzBuzz(int n)
{
size_t size = 0;
for (int base = 1, numOfDigits = 1; base <= n; base *= 10, numOfDigits++) {
int limit = 10 * base - 1;
if (limit > n) {
limit = n;
}
const int ReptCycle = 15;
int reptCycleSize = _computeSizeOfFizzBuzz(base, base + ReptCycle - 1, numOfDigits);
int numOfReptCycles = (limit - base + 1) / ReptCycle;
size += numOfReptCycles * reptCycleSize;
size += _computeSizeOfFizzBuzz(base + ReptCycle * numOfReptCycles, limit, numOfDigits);
}
return size;
}
ssize_t full_write(int fd, const void* buf, size_t nbyte)
{
const char* cbuf = (const char*)buf;
ssize_t total = 0;
while (nbyte > 0) {
ssize_t cc;
do {
cc = write(fd, cbuf, nbyte);
} while (cc < 0 && errno == EINTR);
if (cc < 0) {
return cc;
}
total += cc;
cbuf += cc;
nbyte -= cc;
}
return total;
}
extern const char fizzbuzz[];
__asm(
" .section .rodata.fizzbuzz\n"
" .globl fizzbuzz\n"
"fizzbuzz:\n"
" .incbin \"fizzbuzz.txt\"\n"
);
int main(int argc, char* argv[])
{
unsigned n = argc > 1 ? atoi(argv[1]) : Max;
if (n > Max) {
printf("usage: %s [1..%d]\n", argv[0], Max);
exit(EXIT_FAILURE);
}
size_t size = computeSizeOfFizzBuzz(n);
full_write(STDOUT_FILENO, fizzbuzz, size);
}
# タグに「クソコード」も含めるべきだったかな?
一応の解説
ネタ記事の解説を自らするのは野暮とは思うのですが一応しておきます。
コードを見れば分かる通りですが、1 から 100000000 までの FizzBuzz の出力を文字列として予め持っておき、プログラムの引数の値に応じて出力すべき FizzBuzz の文字列長を求め、その文字列長分 write() で出力しているだけです。
100000000 までの FizzBuzz の文字列は makefile 中の perl のワンライナーでテキストファイルとして生成しており、それをインラインアセンブラで直接インクルードしています。
このコードのキモは 1 から任意の数値までの FizzBuzz の文字列長を求めている部分となります。
FizzBuzz は、数値が15周期で
数値 | 出力される内容 |
---|---|
15の倍数+1 | 数値 |
15の倍数+2 | 数値 |
15の倍数+3 | "Fizz" |
15の倍数+4 | 数値 |
15の倍数+5 | "Buzz" |
15の倍数+6 | "Fizz" |
15の倍数+7 | 数値 |
15の倍数+8 | 数値 |
15の倍数+9 | "Fizz" |
15の倍数+10 | "Buzz" |
15の倍数+11 | 数値 |
15の倍数+12 | "Fizz" |
15の倍数+13 | 数値 |
15の倍数+14 | 数値 |
15の倍数+15 | "FizzBuzz" |
以上のパターンが繰り返されるだけであり、
- 数値の桁数が同一の範囲に於いてはこのパターンのバイト数は常に同じ
であることを利用して出力すべき文字列長を求めています。
このコードの欠点として、プログラム中に 100000000 までの FizzBuzz の結果を文字列として持っているためプログラムサイズは内容の割に少々大きなものとなっていることが挙げられます。
$ ls -l fizzbuzz
-rwxr-xr-x 1 fujita fujita 734085528 Mar 24 01:21 fizzbuzz
$
もっと大きな値まで対応することも可能ですが、プログラムサイズやビルド時間との兼ね合いを考えると現状の 100000000 というのはそこそこ良いところではないかと思います。
おわりに
以上です。