0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

paiza問題集の「一番小さい値」の入力チェック・テストコードを考えてみた。

0
Last updated at Posted at 2025-12-09

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;
}

提出しないコード

リファクタリング
  • 入力部分、ロジック部分、出力部分に分割します
    • 入力に対しては、入力チェックを行います
    • ロジック部分(最小値を求める部分)を切り出し、関数化します

共通関数

今回は、以下の社内標準関数があるということにします。尚、以下のコードを見て指摘事項をコメントに書きたくなる気持ちも分かりますが(書いてくださって結構です)、実際の社内標準関数をここに書くわけにはまいりませんので、ご了承ください。

util.h
#ifndef UTIL_H_
#define UTIL_H_
#include <stdbool.h>

char* chomp(char*);

bool isnumeric(const char*);

#endif /* UTIL_H_ */

util.c
#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を超えていたりしないか

input.h
#ifndef INPUT_H_
#define INPUT_H_
#include <stdbool.h>
#include <stdint.h>

bool parse(const char*, uint8_t*);

#endif /* INPUT_H_ */

input.c
#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*);

が普通でしょうが、別の問題になってきますので、今回は以下のように定義します。


min_num.h
#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_ */

min_num.c
#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関数の戻り値)として返し、直ちにプログラムを終了することとします。


main.c
#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を使います。


inputTest.c
#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環境の場合、以下の様なバッチファイルを作っておくとよいかもしれません。

inputTest.bat
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、入力最大値・最小値をテストします。

minNumTest.c
#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;
}

minNumTest.bat
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つをテストすることにします。


test.c
#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;
	}
}

test.bat
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

次回、御縁と気力がありましたら、数列の最小値をやってみたいと思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?