paizaラーニングレベルアップ問題集の一番小さい値をやってみました。
問題
提出コード
条件分岐は学習したけど、繰り返しや配列は未学習、という人向けです。
- $n_1$を受け取る
- $n_1$を暫定最小値とする
- $n_2$を受け取る
- $n_2$が暫定最小値より小さい場合
- $n_2$を暫定最小値とする
- $n_3$が暫定最小値より小さい場合
- $n_3$を暫定最小値とする
- $n_4$が暫定最小値より小さい場合
- $n_4$を暫定最小値とする
- $n_5$が暫定最小値より小さい場合
- $n_5$を暫定最小値とする
- 暫定最小値を最小値とし、最小値を出力する
#include <stdio.h>
int main() {
int min;
scanf("%d", &min);
int n;
scanf("%d", &n);
if (n < min) {
min = n;
}
scanf("%d", &n);
if (n < min) {
min = n;
}
scanf("%d", &n);
if (n < min) {
min = n;
}
scanf("%d", &n);
if (n < min) {
min = n;
}
printf("%d\n", min);
return 0;
}
提出しないコード
リファクタリング
- 入力部分、ロジック部分、出力部分に分割します
- 入力に対しては、入力チェックを行います
- ロジック部分(最小値を求める部分)を切り出し、関数化します
共通関数
今回は、以下の社内標準関数があるということにします。尚、以下のコードを見て指摘事項をコメントに書きたくなる気持ちも分かりますが(書いてくださって結構です)、実際の社内標準関数をここに書くわけにはまいりませんので、ご了承ください。
#ifndef UTIL_H_
#define UTIL_H_
#include <stdbool.h>
char* chomp(char*);
bool isnumeric(const char*);
#endif /* UTIL_H_ */
#include <string.h>
#include <ctype.h>
#include <stddef.h>
#include "util.h"
char* chomp(char *s) {
if (!s) return NULL;
size_t n = strlen(s);
while (n--)
if (isspace(s[n])) s[n] = '\0';
else break;
return s;
}
bool isnumeric(const char *s) {
if (!(s && *s)) return false;
for (const char *c = s; *c; c++) if (!isdigit(*c)) return false;
return true;
}
入力チェック
今回は、$1\le n_1,n_2,n_3,n_4,n_5\le 100$という制約条件がありますので、入力が条件を満たしているかチェックします。
- (正の)整数値が入力されているか
- 前述の“社内標準関数”を使います
- 1未満だったり、100を超えていたりしないか
#ifndef INPUT_H_
#define INPUT_H_
#include <stdbool.h>
#include <stdint.h>
bool parse(const char*, uint8_t*);
#endif /* INPUT_H_ */
#include <stdlib.h>
#include <errno.h>
#include "util.h"
#include "input.h"
bool parse(const char *str, uint8_t *n) {
errno = 0;
if (!isnumeric(str)) {
errno = EINVAL;
return false;
}
char *s = NULL;
unsigned long u = strtoul(str, &s, 10);
if (errno) {
return false;
} else if (s && *s) {
errno = EINVAL;
return false;
} else if (u < 1 || 100 < u) {
errno = ERANGE;
return false;
}
*n = (uint8_t) u;
return true;
}
ロジック部分
今回の主題である、「5つの正の整数のうち、最も小さい数字を求める」部分を切り出して関数化します。
プロトタイプは
uint8_t minimum(size_t, uint8_t*);
が普通でしょうが、別の問題になってきますので、今回は以下のように定義します。
#ifndef MIN_NUM_H_
#define MIN_NUM_H_
#include <stdint.h>
uint8_t minimum(uint8_t, uint8_t, uint8_t, uint8_t, uint8_t);
#endif /* MIN_NUM_H_ */
#include <errno.h>
#include "min_num.h"
uint8_t minimum(uint8_t n1, uint8_t n2, uint8_t n3, uint8_t n4, uint8_t n5) {
uint8_t min = n1;
if (n2 < min) min = n2;
if (n3 < min) min = n3;
if (n4 < min) min = n4;
if (n5 < min) min = n5;
return min;
}
エントリポイント
最後に、main関数を実装します。
最初にmain関数を実装すると、「minimumという名前は定義されていません」という赤線が出て、たまたま後ろを通りかかった先輩から「赤線出てるよ。大丈夫? 本当に大丈夫?」と言われます。
尚、今回は、不正な入力があった時点でエラーメッセージを出力し、エラーコードを終了コード(main関数の戻り値)として返し、直ちにプログラムを終了することとします。
#include <stdio.h>
#include <stdbool.h>
#include <stdint.h>
#include <errno.h>
#include <assert.h>
#include "min_num.h"
#ifdef NDEBUG
#include "input.h"
#include "util.h"
#endif
int main(void) {
#ifdef NDEBUG
uint8_t n1, n2, n3, n4, n5;
char s[] = "100\n";
if (!(parse(chomp(fgets(s, sizeof(s), stdin)), &n1)
&& parse(chomp(fgets(s, sizeof(s), stdin)), &n2)
&& parse(chomp(fgets(s, sizeof(s), stdin)), &n3)
&& parse(chomp(fgets(s, sizeof(s), stdin)), &n4)
&& parse(chomp(fgets(s, sizeof(s), stdin)), &n5))) {
int e = errno;
perror("The input value must be an integer between 1 and 100.");
return e;
}
printf("%d\n", minimum(n1, n2, n3, n4, n5));
#else
assert(minimum(10, 12, 4, 8, 16) == 4);
assert(minimum(1, 2, 3, 2, 1) == 1);
#endif
return 0;
}
テストコード
これまでの実装と並行して、テスト担当者はテストケースを起こし、テストコードを書いていきます。これは詳細設計書のレビューが通った段階でできるはずです。
今回、“社内標準関数”はテスト済として省略することにします。
入力チェック
入力チェックのテストでは
- (正の)整数値が入力されているか
- 1未満だったり、100を超えていたりしないか
をテストします。特に後者のチェックは、境界値を意識します。
実装を見ると、以下の様なstrtoul関数のエラーの制御もあります。
-
if (error)- 変換できなかった場合
EINVAL -
unsigned longの範囲を超えた場合ERANGE
- 変換できなかった場合
-
else if (s && *s)- 変換できない文字があった場合
EINVAL
- 変換できない文字があった場合
このうち、EINVALになるケースは数字以外の文字列が入ってきた場合なので、“社内標準関数”ではじかれています(のはずですが、念のため制御)。したがって、テストケースからは省略します。
もう一つ、unsigned longの範囲を超える($\ge2^{64}$)場合は100も超えますが、オーバーフローにより0や1になるかもしれない(strtolは$2^{64}-1$を返すことになっているが)ので、こちらはテストしておきます。
今回は、テストフレームワークとしてminunit.hを使います。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#include "minunit.h"
#include "input.h"
int tests_run = 0;
static char* message_hhu(uint8_t expected, uint8_t actual) {
static char msg[80];
snprintf(msg, sizeof(msg), "Error: expected: <%hhu> but was: <%hhu>", expected, actual);
return msg;
}
static char* test_parse(const char *str, uint8_t expected) {
uint8_t actual;
mu_assert("Error: expected: <true> but was: <false>", parse(str, &actual));
mu_assert(message_hhu(expected, actual), actual == expected);
return 0;
}
static char* test_parse_fail(const char *str, int expected) {
uint8_t actual;
mu_assert("Error: expected: <false> but was: <true>", !parse(str, &actual));
mu_assert(message_hhu(expected, errno), errno == expected);
return 0;
}
static char* test_parse_0() {return test_parse_fail(NULL, EINVAL);}
static char* test_parse_1() {return test_parse_fail("", EINVAL);}
static char* test_parse_2() {return test_parse("1", 1);}
static char* test_parse_3() {return test_parse_fail("0", ERANGE);}
static char* test_parse_4() {return test_parse("100", 100);}
static char* test_parse_5() {return test_parse_fail("101", ERANGE);}
static char* test_parse_6() {return test_parse_fail("18446744073709551616", ERANGE);}
static char* all_tests() {
mu_run_test(test_parse_0);
mu_run_test(test_parse_1);
mu_run_test(test_parse_2);
mu_run_test(test_parse_3);
mu_run_test(test_parse_4);
mu_run_test(test_parse_5);
mu_run_test(test_parse_6);
return 0;
}
int main() {
char *result = all_tests();
if (result != 0) {
fprintf(stderr, "%s\n", result);
fprintf(stderr, "Tests run: %d\n", tests_run);
} else {
fprintf(stdout, "ALL TESTS PASSED\n");
fprintf(stdout, "Tests run: %d\n", tests_run);
}
return result != 0;
}
Windows環境の場合、以下の様なバッチファイルを作っておくとよいかもしれません。
echo off
gcc -Wall -Wextra -Werror -std=c99 inputTest.c input.c util.c -o inputTest 1>inputTest.txt 2>&1
if exist inputTest.exe (
.\inputTest
echo %ERRORLEVEL%
del inputTest.exe
del inputTest.txt
) else (
type inputTest.txt
)
pause
ロジック部テスト
本来であれば「分岐網羅」とか「境界値分析」とか意識すべきですが、今回は単純に
- $n_1$が最小
- $n_2$が最小
- $n_3$が最小
- $n_4$が最小
- $n_5$が最小
の場合をテストします。
あとは入出力例1,2、入力最大値・最小値をテストします。
#include <stdio.h>
#include <stdint.h>
#include <errno.h>
#include "minunit.h"
#include "min_num.h"
int tests_run = 0;
static char* message_hhu(uint8_t expected, uint8_t actual) {
static char msg[80];
snprintf(msg, sizeof(msg), "Error: expected: <%hhu> but was: <%hhu>", expected, actual);
return msg;
}
static char* test_minimum(uint8_t n1, uint8_t n2, uint8_t n3, uint8_t n4, uint8_t n5, uint8_t expected) {
uint8_t actual = minimum(n1, n2, n3, n4, n5);
mu_assert(message_hhu(expected, actual), actual == expected);
return 0;
}
static char* test_minimum_1() {return test_minimum(10, 12, 4, 8, 16, 4);}
static char* test_minimum_2() {return test_minimum(1, 2, 3, 2, 1, 1);}
static char* test_minimum_3() {return test_minimum(2, 30, 58, 66, 94, 2);}
static char* test_minimum_4() {return test_minimum(98, 6, 34, 42, 70, 6);}
static char* test_minimum_5() {return test_minimum(74, 82, 10, 38, 46, 10);}
static char* test_minimum_6() {return test_minimum(50, 78, 86, 14, 22, 14);}
static char* test_minimum_7() {return test_minimum(26, 54, 62, 90, 18, 18);}
static char* test_minimum_8() {return test_minimum(1, 1, 1, 1, 1, 1);}
static char* test_minimum_9() {return test_minimum(100, 100, 100, 100, 100, 100);}
static char* all_tests() {
mu_run_test(test_minimum_1);
mu_run_test(test_minimum_2);
mu_run_test(test_minimum_3);
mu_run_test(test_minimum_4);
mu_run_test(test_minimum_5);
mu_run_test(test_minimum_6);
mu_run_test(test_minimum_7);
mu_run_test(test_minimum_8);
mu_run_test(test_minimum_9);
return 0;
}
int main() {
char *result = all_tests();
if (result != 0) {
fprintf(stderr, "%s\n", result);
fprintf(stderr, "Tests run: %d\n", tests_run);
} else {
fprintf(stdout, "ALL TESTS PASSED\n");
fprintf(stdout, "Tests run: %d\n", tests_run);
}
return result != 0;
}
echo off
gcc -Wall -Wextra -Werror -std=c99 minNumTest.c min_num.c -o minNumTest 1>minNumTest.txt 2>&1
if exist minNumTest.exe (
.\minNumTest
echo %ERRORLEVEL%
del minNumTest.exe
del minNumTest.txt
) else (
type minNumTest.txt
)
pause
エントリポイントテスト
main関数のテストをします。ちょと危険なやり方ですが、テストコード内でプログラムをコンパイルし、そのプログラムを呼び出すことにします。
入出力例1,2と、不正な入力パターン5つをテストすることにします。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <assert.h>
#include <errno.h>
#include "minunit.h"
#define SOURCE "main"
#define IN_FILE "in.txt"
#define OUT_FILE "out.txt"
#define ERR_FILE "err.txt"
int tests_run = 0;
char result[1][8];
char msg[80];
static char* message_zu(size_t expected, size_t actual) {
sprintf(msg, "Error: expected: <%zu> but was: <%zu>", expected, actual);
return msg;
}
static char* message_s(const char *expected, const char *actual) {
sprintf(msg, "Error: expected: <%s> but was: <%s>", expected, actual);
return msg;
}
static char* message_d(int expected, int actual) {
sprintf(msg, "Error: expected: <%d> but was: <%d>", expected, actual);
return msg;
}
static size_t test(size_t n, const char *lines[]) {
static unsigned k = 0;
FILE *file;
if (!(file = fopen(IN_FILE, "w"))) {
fprintf(stderr, "%s\n", strerror(errno));
exit(errno);
}
for (size_t i = 0; i < n; i++) {
fputs(lines[i], file);
fputc('\n', file);
}
fclose(file);
clock_t start = clock();
#if defined(_WIN32) || defined(_WIN64)
if (!!(errno = system(".\\" SOURCE " <" IN_FILE " 1>" OUT_FILE " 2>" ERR_FILE))) {
#else
if (!!(errno = system("./" SOURCE " <" IN_FILE " 1>" OUT_FILE " 2>" ERR_FILE))) {
#endif
fprintf(stderr, "%s\n", strerror(errno));
exit(errno);
}
fprintf(stderr, "#%u %f sec\n", ++k, (float) (clock() - start) / CLOCKS_PER_SEC);
fflush(stderr);
if (!(file = fopen(OUT_FILE, "r"))) {
fprintf(stderr, "%s\n", strerror(errno));
exit(errno);
}
size_t m = 0;
while (fgets(result[m], sizeof(result[m]), file)) m++;
fclose(file);
return m;
}
static int test_err(size_t n, const char *lines[]) {
FILE *file;
if (!(file = fopen(IN_FILE, "w"))) {
fprintf(stderr, "%s\n", strerror(errno));
exit(errno);
}
if (lines) {
for (size_t i = 0; i < n; i++) {
if (lines[i]) fputs(lines[i], file);
fputc('\n', file);
}
}
fclose(file);
#if defined(_WIN32) || defined(_WIN64)
return system(".\\" SOURCE " <" IN_FILE " 1>" OUT_FILE " 2>" ERR_FILE);
#else
return system("./" SOURCE " <" IN_FILE " 1>" OUT_FILE " 2>" ERR_FILE);
#endif
}
static char* test1() {
const char *lines[] = { "10", "12", "4", "8", "16" };
size_t n = test(sizeof(lines) / sizeof(lines[0]), lines);
mu_assert(message_zu(1, n), n == 1);
mu_assert(message_s("4\n", result[0]), !strcmp(result[0], "4\n"));
return 0;
}
static char* test2() {
const char *lines[] = { "1", "2", "3", "2", "1" };
size_t n = test(sizeof(lines) / sizeof(lines[0]), lines);
mu_assert(message_zu(1, n), n == 1);
mu_assert(message_s("1\n", result[0]), !strcmp(result[0], "1\n"));
return 0;
}
static char* test3() {
const char *lines[] = { "", "0", "0", "0", "0" };
int e = test_err(sizeof(lines) / sizeof(lines[0]), lines);
mu_assert(message_d(EINVAL, e), e == EINVAL);
return 0;
}
static char* test4() {
const char *lines[] = { "1", " ", "0", "0", "0" };
int e = test_err(sizeof(lines) / sizeof(lines[0]), lines);
mu_assert(message_d(EINVAL, e), e == EINVAL);
return 0;
}
static char* test5() {
const char *lines[] = { "1", "1", " ", "0", "0" };
int e = test_err(sizeof(lines) / sizeof(lines[0]), lines);
mu_assert(message_d(EINVAL, e), e == EINVAL);
return 0;
}
static char* test6() {
const char *lines[] = { "1", "1", "1", "0", "" };
int e = test_err(sizeof(lines) / sizeof(lines[0]), lines);
mu_assert(message_d(ERANGE, e), e == ERANGE);
return 0;
}
static char* test7() {
const char *lines[] = { "1", "1", "1", "1", "101" };
int e = test_err(sizeof(lines) / sizeof(lines[0]), lines);
mu_assert(message_d(ERANGE, e), e == ERANGE);
return 0;
}
static char* all_tests() {
mu_run_test(test1);
mu_run_test(test2);
mu_run_test(test3);
mu_run_test(test4);
mu_run_test(test5);
mu_run_test(test6);
mu_run_test(test7);
return 0;
}
int main() {
if (!!(errno = system("gcc -Wall -Wextra -Werror -std=c99 -DNDEBUG " SOURCE ".c input.c util.c min_num.c -o " SOURCE " 1>" SOURCE ".txt 2>&1"))) {
fprintf(stderr, "%s\n", strerror(errno));
FILE* file = fopen(SOURCE ".txt", "r");
if (file) {
int c = 0;
while ((c = fgetc(file)) != EOF) {
putchar(c);
}
fclose(file);
}
exit(errno);
}
char *result = all_tests();
if (result != 0) {
fprintf(stderr, "%s\n", result);
fprintf(stderr, "Tests run: %d\n", tests_run);
} else {
fprintf(stdout, "ALL TESTS PASSED\n");
fprintf(stdout, "Tests run: %d\n", tests_run);
}
remove(ERR_FILE);
remove(OUT_FILE);
remove(IN_FILE);
remove(SOURCE ".exe");
remove(SOURCE ".txt");
if (errno) {
return errno;
} else {
return result != 0;
}
}
echo off
gcc -Wall -Wextra -Werror -std=c99 test.c -o test 1>test.txt 2>&1
if exist test.exe (
.\test
echo %ERRORLEVEL%
del test.exe
del test.txt
) else (
type test.txt
)
pause
次回、御縁と気力がありましたら、数列の最小値をやってみたいと思います。