【C言語】「ＩＰアドレスのフォーマットチェック」と言われて、どう考えるか

僕は今、大学でプログラミングの勉強をしています。最近ゼミの配属先が決まり、開発言語の習得に本腰を入れ始めています。しかし、一方で、おおよその構造や使うべき場面を把握し、自分で納得して使えるようなincludeファイルは、`<stdio.h>` `<iostream>` `<string.h>`……といった、まだいろはの授業で使うような基礎的なものしかありません。

そんな状況で、この間、問題「IPアドレスのフォーマットチェックをするプログラムを作れ」に遭いました。検索してみても、`<winsock2.h>`や`<netdb.h>` `<arpa/inet.h>`等、見慣れないファイルが次々見つかるばかり。そういった「ザ・IPアドレス」な専門知識がなければ解けない問題なのかと、実力に限界を感じて挫折しかけています。
IPアドレスのフォーマットチェックをしろと言われて、文字通りに解釈して難しく考えるのか、もしくはこれまでに得た知識で対抗出来るのか、何日も考えていました。

ある日、文字を構成する要素を制限していく方向で考える方法を思いつきました。IPアドレスを、ただの文字列としてとらえるという事になったのです。まず「IPアドレスには、少なくともアルファベットは入らないから、読み込んだ文字列にアルファベットが紛れていたらエラーになるようにしよう」と思い、**文字列を１文字１文字調べる** (➀) if文を作成。次に「文字”.”はIPアドレスの文字列に３つしか存在しない」と判断し、➀１回につき要素が”.”かを調べ、**該当すればカウントに入れ、ループ後にカウント数が３だったらパスする**if文を作成。最後に「”.”はIPアドレスにおいて、文字列の端につくものではない」と思い、両端のどちらか一方でも”.”であればエラーとするif文を作成しました。
これを繋ぎ合わせて、ゴリ押しのような形で１つのプログラムに仕上げました。
文字列を同時に３パターン入力し、パターンそれぞれを調べてもらおうというものです。

```c:間違ったプログラム
#include <stdio.h>
#include <string.h>

int main(){
	
	char buf[1000], str[3][50];
	for(int i = 0; i < 3; i++){
		fgets(buf, sizeof(buf), stdin);
		sscanf(buf, "%s", str[i]);
	}
	
	for(int ver = 0; ver < 3; ver++){
		
		int number = 0, count = 0;
		while(number < strlen(str[ver])){
			
			if(str[ver][number] < '0' || str[ver][number] > '9'){
				if(str[ver][number] != '.'){
					printf("%d番目のIP：無効なフォーマット\n", ver + 1);
					return 0;
				}
			}
			if(str[ver][number] == '.'){
				count++;
			}
			number++;
			
		}
		
		if(count != 3 || str[ver][0] == '.' || str[ver][strlen(str[ver])-1] == '.'){
			printf("%d番目のIP：無効なフォーマット\n", ver + 1);
		}else{
			printf("%d番目のIP：使用可能\n", ver + 1);
		}
		
	}
	
	return 0;
	
}
```

しかし、プログラム名からも分かるように、このプログラムにはまだ欠陥がありました。
僕が確認出来ただけでも２つです。

:::note warn
**・そもそも、IPアドレスを構成する各々の数字は0～255に限定されていた**
**・「12.34..56」という入力が通ってしまう**
:::

このように、文字の「並び」にも着目する必要があったのです。簡単に解こうとすると、さらに泥沼化しそうな予感。
そうなると、やっぱり専門知識を使った正攻法を使わないと解けないのか！と泣きたくなりました。

その夜、strtokを使って文字列を分割し、int型に変換して0～255を満たすかどうかを調べてみる事にしました。

```c:まだ違うプログラム
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(){
	
	char buf[100], ipadd[50];
	fgets(buf, sizeof(buf), stdin);
	sscanf(buf, "%s", ipadd);
	
	char *numary = strtok(ipadd, ".");
	while(numary != NULL){
		printf("%s\n", numary);
		int n = atoi(numary);
		if(n < 0 || n > 255 || numary == NULL){
		    printf("0～255を満たさない数字あり\n");
			return 0;
		}else{
			
		}
		numary = strtok(NULL, ".");
	}
	
	printf("範囲OK\n");
	
}
```

数字の範囲をカバーするだけなら問題なかったです。しかし、これだとまだ「12.34..56」という入力が通ってしまいます。「”.”が２個連続していた場合に、その２つの間のNULL文字を読み取らせて異常終了させる」のを狙ってif文の条件式に入れた`numary == NULL`は全く効果がありませんでした。

と、ここで一つ思い出しました。以前[四捨五入についての記事](https://qiita.com/weemiee/items/161cdfbfa72101c26045)を書いた事があって、そこで小数点の位置をstr[X]というように、ある文字の有無の特定や (あったとして) 何番目にあるかを突き止める事が出来た記憶があったのです。

```c
char *p = strchr(ipadd, '.'), *str = ipadd;
if(p != NULL){
	printf("%td\n", p-str);
}
```

さっそく応用する時が来ました。プログラミングの勉強をしていて唯一やりがいを感じるのは、地道にインプットしてきた知識をアウトプット出来る場面に至った時です。

ただし、一つ注意すべきは、`strchr(buffer, '.')`をループの度に定義し直さないと、同じ位置 (p-strの値が変わらない) のまま繰り返されてしまう点です。そのため、`char *p = strchr(buffer, '.');`はwhile文の中に入れます。

```c
int main(){
	
	//ここに標準入力を行うプログラムを入れる
	
	char buffer[50] = "";
	strcpy(buffer, ipadd);
	printf("文字列：%s\n",buffer);
	char *str = buffer;
	int i = 0;
	while(i < strlen(buffer)){
		char *p = strchr(buffer, '.');
		if(p != NULL){
			if(buffer[p-str+1] == '.'){   //もし、その隣の文字も「.」だったら……
				printf("「.」が連続しています\n");
				return 0;
			}else{
				printf("位置【%td】 分割文字【%c】\n", p-str, buffer[p-str]);
			}
			buffer[p-str] = ',';
		}
		i++;
	}
	
	//ここに分割処理を行うプログラムを入れる
	
}
```

ここでついに、今までゴチャゴチャだったのが全て繋がりました。
まとめて１つのプログラムに仕上げます。

```c:完成プログラム
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(){
	
	char buf[100], ipadd[50], buffer[50] = "";
	fgets(buf, sizeof(buf), stdin);
	sscanf(buf, "%s", ipadd);
	
	//文字列から、IPアドレスに関係のないアルファベット等の不純物を判別する（2022/10/28に追加）
	int num = 0;
	while(num < strlen(ipadd)){
		if(ipadd[num] < '0' || ipadd[num] > '9'){
			if(ipadd[num] == '.'){
				
			}else{
				printf("使用不可\n");
				return 0;
			}
		}
		num++;
	}
	
	//「.」連続の有無を調べる
	strcpy(buffer, ipadd);
	//printf("文字列：%s\n",buffer);   //任意で挿入
	char *str = buffer;
	int i = 0, count = 0;
	while(i < strlen(buffer)){
		char *p = strchr(buffer, '.');
		if(p != NULL){
			if(buffer[p-str+1] == '.'){   //もし、その隣の文字も「.」だったら……
				printf("使用不可\n");
				return 0;
			}else{
				//printf("位置【%td】 分割文字【%c】\n", p-str, buffer[p-str]);   //任意で挿入
			}
			buffer[p-str] = '@';
			count++;   //ついでに「.」の出現数をカウント
		}
		i++;
	}
	
	//分割して数字化し、範囲内であるか調べる
	char buffer2[50] = "";
	strcpy(buffer2, ipadd);
	char *numary = strtok(buffer2, ".");
	while(numary != NULL){
		int n = atoi(numary);
		if(n < 0 || n > 255){
			printf("使用不可\n");
			return 0;
		}
		numary = strtok(NULL, ".");
	}
	
	//残りの判断基準で篩にかける
	if(count != 3 || ipadd[0] == '.' || ipadd[strlen(ipadd) - 1] == '.'){
		printf("使用不可\n");
	}else{
		printf("使用可能\n");
	}
	
}
```

結果的にプログラム構成が複雑にはなったものの、専門用語は極力回避して作れたので、僕としては満足しています。
IPアドレスのフォーマットチェックとはいえ、今までの知識で十分でした。ただ、考える事が多いので、作成に膨大な時間をかけてしまったのが課題点でした。
PythonやJavaでもっと簡単に書ける場合、ぜひご教授お願いします。
編集リクエストの方もお待ちしています。