Ｃで文字列分割関数を自作してみた

Ｃ言語には文字列分割関数が無い！？　ということで、元の文字列を保持したまま分割文字列を出力する自作することにしてみました。
- - -
- `strtok`関数を使う（元の文字列は保持されない）
```split.h
#ifndef _SPLIT_H_
#define _SPLIT_H_
int split(char* dst[], char* src, const char* delim);
#endif //_SPLIT_H_
```
```split.c
#include <string.h>
#include "split.h"

/*
 * 入力された文字列を、指定された文字群に一致する位置で分割し、分割後の文字列数を返す。
 * dst : 分割後の文字列の先頭を指すポインタ
 * src : 分割前の文字列
 * delim : 区切り文字群
 * 返却値 : 分割後の文字列数
 */
int split(char* dst[], char* src, const char* delim){
    int n=0;
    char* s=strtok(src, delim);
    while(s){
        dst[n++]=s;
        s=strtok(NULL, delim);
    }
    return n;
}
```
:::note
`split`関数適用後、
```c
char* s=src;
s+=(strlen(src)+1);
...
```
でアクセスしていくことは可能。
:::
- - -
- 区切り文字は一種類
```split.h
#ifndef _SPLIT_H_
#define _SPLIT_H_
int split(char* dst[], const char* src, char delim);
#endif //_SPLIT_H_
```
```split.c
#include "split.h"

/*
 * 入力された文字列を、指定された文字に一致する位置で分割し、分割後の文字列数を返す。
 * dst : 分割後の文字列の先頭を指すポインタ
 * src : 分割前の文字列
 * delim : 区切り文字
 * 返却値 : 分割後の文字列数
 */
int split(char* dst[], const char* src, char delim){
    //分割後の文字列数
    int n=0;

    //n番目の返却文字列のj文字目に出力文字を格納していく
    int j=0;

    //入力文字列の終端文字まで繰り返し
    for(int i=0;src[i]!='\0';i++){

        //区切り文字を探索
        if(src[i]==delim){//区切り文字があった場合
            //出力文字列配列の該当位置に終端文字を格納
            dst[n++][j]='\0';

            //格納位置をリセット
            j=0;
        }else{
            //区切り文字以外はそのまま格納
            dst[n][j++]=src[i];
        }
    }
    
    //最後に終端文字を格納
    dst[n++][j]='\0';

    //実際の分割後の文字列数を返す
    return n;
}
```
- - -
- 関数とポインタを用いる
```split.h
#ifndef _SPLIT_H_
#define _SPLIT_H_
int split(char* dst[], const char* src, char delim);
#endif //_SPLIT_H_
```
```split.c
#include <string.h>
#include "split.h"

/*
 * 入力された文字列を、指定された文字に一致する位置で分割し、分割後の文字列数を返す。
 * dst : 分割後の文字列の先頭を指すポインタ
 * src : 分割前の文字列
 * delim : 区切り文字
 * 返却値 : 分割後の文字列数
 */
int split(char* dst[], const char* src, char delim){
    //分割後の文字列数
    int n=0;

    //入力文字列の先頭から、区切り文字を探索
    const char* s=src;
    char* c=strchr(s, delim);
    
    while(c){//区切り文字がある限り繰り返し

        //区切り文字までの文字列を出力文字列にコピー
        strncpy(dst[n],s,c-s);

        //終端文字を格納
        dst[n++][c-s]='\0';

        //区切り文字の直後から、次の区切り文字を探索
        s=c+1;
        c=strchr(s, delim);
    }

    //残りの文字列をコピー
    strcpy(dst[n],s);

    //終端文字を格納
    dst[n++][strlen(s)]='\0';

    //実際の分割後の文字列数を返す
    return n;
}
```
- - -
- 区切り文字は複数種類
```split.h
#ifndef _SPLIT_H_
#define _SPLIT_H_
int split(char* dst[], const char* src, const char* delim);
#endif //_SPLIT_H_
```
```split.c
#include <string.h>
#include "split.h"

/*
 * 入力された文字列を、指定された文字群に一致する位置で分割し、分割後の文字列数を返す。
 * dst : 分割後の文字列の先頭を指すポインタ
 * src : 分割前の文字列
 * delim : 区切り文字群
 * 返却値 : 分割後の文字列数
 */
int split(char* dst[], const char* src, const char* delim){
    //分割後の文字列数
    int n=0;

    //入力文字列の先頭から、区切り文字を探索
    const char* s=src;

    //strchrに代わってstrpbrkを用いる
    char* c=strpbrk(s, delim);
    
    while(c){//区切り文字がある限り繰り返し
        
        //区切り文字までの文字列を出力文字列にコピー
        strncpy(dst[n],s,c-s);

        //終端文字を格納
        dst[n++][c-s]='\0';

        //区切り文字の直後から、次の区切り文字を探索
        s=c+1;

        //strchrに代わってstrpbrkを用いる
        c=strpbrk(s, delim);
    }

    //残りの文字列をコピー
    strcpy(dst[n],s);

    //終端文字を格納
    dst[n++][strlen(s)]='\0';

    //実際の分割後の文字列数を返す
    return n;
}
```
- - -
- 区切り文字が引用符で囲まれている場合も想定
:::note
制約条件
- 開き引用符は文字列の先頭または区切り文字の次
- 開き引用符の後には対応する閉じ引用符が存在する
- 開き引用符と閉じ引用符の間にある2個連続した引用符で引用符1個を表す
- 閉じ引用符は文字列の末尾、または閉じ引用符の直後に区切り文字
:::
```split.h
#ifndef _SPLIT_H_
#define _SPLIT_H_
int split(char* dst[], const char* src, const char* delim, char quote);
#endif //_SPLIT_H_
```
```split.c
#include <string.h>
#include <stdlib.h>
#include "split.h"

int split(char* dst[], const char* src, const char* delim, char quote){
    //分割後の文字列数
    int n=0;

    //入力文字列の先頭から、区切り文字や引用符を探索
    const char* s=src;

    for(;;){//(*1)
        //出力領域を確実に空文字列で初期化
        strcpy(dst[n],"");
        
        if(*s==quote){//開き引用符
            //次の引用符が閉じなのか2個続きで1個の引用符を表すのか不明なので、無限ループ処理としておく
            for(;;){//(*2)
                //閉じ引用符候補を探索（無ければSegmentation faultで落とす）
                char* c=strchr(s+1,quote);

                //開き引用符直後から閉じ引用符直前までの文字列を結合
                strncat(dst[n],s+1,c-s-1);

                //閉じ引用符の次の文字
                s=c+1;
                if(*s==quote){//引用符が2個続いた
                    //引用符を1個結合
                    strncat(dst[n],s,1);
                }else{
                    //*cは閉じ引用符だったので、(*2)のループ脱出
                    break;
                }
            }//(*2)
            if(*s=='\0'){
                //閉じ引用符で文字列が終了した場合、(*1)のループ脱出
                break;
            }
            if(strchr(delim,*s)){//閉じ引用符の次が区切り文字
                //次の探索位置を進める
                s++;
            }else{
                //閉じ引用符の次が区切り文字でも終端文字でもない場合、不正
                abort();
            }
        }else{//引用符以外
            //区切り文字の直後または文字列の先頭から、次の区切り文字を探索
            char* c=strpbrk(s, delim);
            
            if(!c){//区切り文字がない
                //残りの文字列を結合
                strcat(dst[n],s);

                //(*1)のループから脱出
                break;
            }

            //区切り文字直前までの文字列を結合
            strncat(dst[n],s,c-s);

            //次の探索位置は区切り文字の次
            s=c+1;
        }

        //分割後の文字列数をインクリメントし、次の出力領域に格納していく
        n++;
    }//(*1)

    //最後にインクリメントし、実際の分割後の文字列数を返す
    return ++n;
}
```
- - -
- 引用符も複数種類
:::note
制約条件
- 開き引用符は文字列の先頭または区切り文字の次
- 開き引用符の後には対応する同じ閉じ引用符が存在する
- 開き引用符と閉じ引用符の間にある2個連続した同じ引用符でその引用符1個を表す
- 閉じ引用符は文字列の末尾、または閉じ引用符の直後に区切り文字
:::

```split.h
#ifndef _SPLIT_H_
#define _SPLIT_H_
int split(char* dst[], const char* src, const char* delim, const char* quote);
#endif //_SPLIT_H_
```
```split.c
#include <string.h>
#include <stdlib.h>
#include "split.h"

int split(char* dst[], const char* src, const char* delim, const char* quote){
    //分割後の文字列数
    int n=0;
    //入力文字列の先頭から、区切り文字や引用符を探索
    const char* s=src;

    for(;;){//(*1)
        //出力領域を確実に空文字列で初期化
        strcpy(dst[n],"");

        //開き引用符チェック
        char* q=strchr(quote,*s);
        
        if(q && *q){//開き引用符だった場合
            //次の引用符が閉じなのか2個続きで1個の引用符を表すのか不明なので、無限ループ処理としておく
            for(;;){//(*2)
                //閉じ引用符候補を探索（同じ引用符で探索。無ければSegmentation faultで落とす）
                char* c=strchr(s+1,*s);

                //開き引用符直後から閉じ引用符直前までの文字列を結合
                strncat(dst[n],s+1,c-s-1);

                //閉じ引用符の次の文字
                s=c+1;
                if(*s==*q){//同じ引用符が2個続いた
                    //引用符を1個結合
                    strncat(dst[n],s,1);
                }else{
                    //*cは閉じ引用符だったので、(*2)のループ脱出
                    break;
                }
            }
            if(*s=='\0'){
                //閉じ引用符で文字列が終了した場合、(*1)のループ脱出
                break;
            }
            if(strchr(delim,*s)){//閉じ引用符の次が区切り文字
                //次の探索位置を進める
                s++;
            }else{
                //閉じ引用符の次が区切り文字でも終端文字でもない場合、不正
                abort();
            }
        }else{//引用符以外
            //区切り文字の直後または文字列の先頭から、次の区切り文字を探索
            char* c=strpbrk(s, delim);
            
            if(!c){//区切り文字がない
                //残りの文字列を結合
                strcat(dst[n],s);

                //(*1)のループから脱出
                break;
            }

            //区切り文字直前までの文字列を結合
            strncat(dst[n],s,c-s);

            //次の探索位置は区切り文字の次
            s=c+1;
        }

        //分割後の文字列数をインクリメントし、次の出力領域に格納していく
        n++;
    }//(*1)

    //最後にインクリメントし、実際の分割後の文字列数を返す
    return ++n;
}
```
GitHub=>[split.h](https://github.com/anonymously115/repository115/blob/main/split.h)
GitHub=>[split.c](https://github.com/anonymously115/repository115/blob/main/split.c)
- - -
- サンプル：CSVファイルを読み込んで表示するプログラム
```main.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "split.h"

/* 区切り文字 */
char* DELIMITER=",";

/* 引用符 */
char* QUOTATION="\"";

/*
 * コマンドラインの第1引数に指定されたファイルをcsv形式だと思って読み込み、画面に表示する
 */
int main(int argc, char* argv[]){
    if(argc<2){//ファイルが指定されていない
        fprintf(stderr, "%s: fatal error: no input files\n", argv[0]);
        abort();
    }

    //ファイルオープン
    FILE* file=fopen(argv[1],"r");
    if(!file){//ファイルが存在しない
        fprintf(stderr, "%s: error: %s: No such file or directory\n", argv[0], argv[1]);
        fprintf(stderr, "%s: fatal error: no input files\n", argv[0]);
        abort();
    }

    while(!feof(file)){//ファイル末尾まで読み込む
        //１行読み込む
        char buf[BUFSIZ];
        char* line=fgets(buf,sizeof(buf),file);
        int e=ferror(file);
        if(e){//エラー発生
            fprintf(stderr, "%s\n", strerror(e));
            exit(e);
        }
        if(!line){//ファイル終端の場合、ループ終了
            break;
        }

        //改行コードを削除（Windowsの改行は「CR+LF」（"\r\n"）なので、'\n'→'\r'の順で末尾から削除）
        if(line[strlen(line)-1]=='\n')line[strlen(line)-1]='\0';
        if(line[strlen(line)-1]=='\r')line[strlen(line)-1]='\0';

        //仮の分割後文字数
        int m=1;

        //区切り文字を探してインクリメント
        char* c=strpbrk(line,DELIMITER);
        while(c){
            m++;
            c=strpbrk(c+1,DELIMITER);
        }

        //出力領域を確保
        char dst[m][BUFSIZ];
        char* p_dst[m];
        for(int i=0;i<m;i++){
            p_dst[i]=dst[i];
        }

        //分割実行（区切り文字はカンマ、引用符はダブルクォーテーションを指定）
        m=split(p_dst,line,DELIMITER,QUOTATION);

        //結果を画面表示
        for(int i=0;i<m;i++){
            printf("|%s",dst[i]);
        }
        printf("|\n");
    }

    //ファイルクローズ
    fclose(file);

    //プログラム終了
    return 0;
}
```
- テストデータ

||||
|:-:|:-:|:-:|
| " | , |   |
| " |   | , |
| , | " |   |
| , |   | " |
|   | " | , |
|   | , | " |
```test.csv
"""",",",
"""",,","
",","""",
",",,""""
,"""",","
,",",""""
```
- コンパイル＆テスト
```
gcc main.c split.c -o split
split test.csv
|"|,||
|"||,|
|,|"||
|,||"|
||"|,|
||,|"|
```
- - -
:::note alert
Ｃ言語では
```c
char** split(const char* src, const char* token){
	char dst[strlen(src)+1]; //関数内で配列を宣言
	strcpy(dst,src);
	int n=1;
	for(const char* c=src;*c;c++){
		if(strchr(token,*c))n++;
	}
	char* res[n]; //関数内で配列を宣言
	res[0]=strtok(dst,token); //関数内で宣言した配列を指すポインタ
	for(int i=1;i<n;i++){
		res[i]=strtok(NULL,token);
	}
	return res; //関数内で宣言した配列を返す
}
```
のように、関数内で宣言した配列を返すことはできません。
更に、
```main.c
#include <stdio.h>

int main(int argc,char* argv[]){
    char* dst[]=split("hello,world",",");
    for(int i=0;i<sizeof(dst)/sizeof(dst[0]);i++){
        printf("%s\n",dst[i]);
    }
    return 0;
}
```
などと書いても、コンパイルすら通りません。
:::