第3章:ファイル解析とデータ構造

3.1 FdFマップファイルの形式

3.1.1 ファイルフォーマットの理解

FdFのマップファイルは、シンプルなテキスト形式で3D地形データを表現します。

マップファイルの構造:

ファイル名: 42.fdf

内容:
0  0  0  0  0  0  0  0  0  0
0  0  0  0  0  0  0  0  0  0
0  0 10 10  0  0 10 10  0  0
0  0 10 10  0  0 10 10  0  0
0  0  0  0 10 10  0  0  0  0
0  0  0  0 10 10  0  0  0  0
0  0 10 10  0  0 10 10  0  0
0  0 10 10  0  0 10 10  0  0
0  0  0  0  0  0  0  0  0  0
0  0  0  0  0  0  0  0  0  0

解釈:
- 各数値 = 高さ(Z座標)
- 列番号 = X座標
- 行番号 = Y座標
- スペース区切り
- 改行で次の行へ

色情報付きフォーマット:

0,0xFF0000  0  0  0  0
0  10,0x00FF00  0  0  0
0  0  0  20,0x0000FF  0

形式: 高さ,0xRRGGBB

高さと色をカンマで区切り、16進数で色を指定
色情報がない場合はデフォルト色を使用

3.1.2 パースの課題

パースで考慮すべき問題:

1. 不正なファイル
   ├── ファイルが存在しない
   ├── 読み取り権限がない
   └── 空ファイル

2. 不正なデータ
   ├── 数値以外の文字
   ├── 行ごとに列数が異なる
   └── 負の数や極端に大きな数

3. メモリ管理
   ├── 巨大なファイル
   ├── メモリ確保失敗
   └── メモリリーク防止

4. エッジケース
   ├── 1x1マップ
   ├── 1行のみ
   └── 1列のみ

3.2 ファイル読み込みの実装

3.2.1 get_next_lineの活用

/*
 * read_file - ファイル全体を読み込み
 *
 * get_next_line を使用して1行ずつ読み込み、
 * 連結リストに格納
 */
typedef struct s_line
{
    char            *content;
    struct s_line   *next;
}   t_line;

t_line *read_file(const char *filename)
{
    int     fd;
    char    *line;
    t_line  *head;
    t_line  *current;

    fd = open(filename, O_RDONLY);
    if (fd < 0)
        return (error_null("Cannot open file"));

    head = NULL;
    while ((line = get_next_line(fd)) != NULL)
    {
        t_line *new_node = malloc(sizeof(t_line));
        if (!new_node)
        {
            free(line);
            free_lines(head);
            close(fd);
            return (NULL);
        }
        new_node->content = line;
        new_node->next = NULL;

        if (!head)
            head = new_node;
        else
            current->next = new_node;
        current = new_node;
    }

    close(fd);
    return (head);
}

3.2.2 ファイル検証

/*
 * validate_file - ファイルの事前検証
 */
int validate_file(const char *filename)
{
    int         fd;
    struct stat st;

    /* ファイル存在確認 */
    if (access(filename, F_OK) != 0)
        return (error_return("File does not exist", 0));

    /* 読み取り権限確認 */
    if (access(filename, R_OK) != 0)
        return (error_return("No read permission", 0));

    /* 通常ファイルか確認 */
    if (stat(filename, &st) != 0)
        return (error_return("Cannot stat file", 0));

    if (!S_ISREG(st.st_mode))
        return (error_return("Not a regular file", 0));

    /* 空ファイル確認 */
    if (st.st_size == 0)
        return (error_return("Empty file", 0));

    /* .fdf拡張子確認(オプション) */
    if (!has_extension(filename, ".fdf"))
        return (error_return("Invalid file extension", 0));

    return (1);
}

/*
 * 拡張子チェック
 */
int has_extension(const char *filename, const char *ext)
{
    size_t  name_len;
    size_t  ext_len;

    name_len = ft_strlen(filename);
    ext_len = ft_strlen(ext);

    if (name_len < ext_len)
        return (0);

    return (ft_strcmp(filename + name_len - ext_len, ext) == 0);
}

3.3 数値パースの実装

3.3.1 ft_atoiの拡張

/*
 * parse_point - 1つの点データをパース
 *
 * 入力形式:
 *   "10"          → z=10, color=default
 *   "10,0xFF0000" → z=10, color=0xFF0000
 */
int parse_point(const char *str, t_point *point)
{
    char    *comma;
    char    *endptr;

    /* 高さ(Z値)のパース */
    point->z = ft_strtol(str, &endptr);
    if (endptr == str)
        return (0);  /* 数値なし */

    /* 色のパース */
    if (*endptr == ',')
    {
        comma = endptr + 1;
        if (comma[0] == '0' && (comma[1] == 'x' || comma[1] == 'X'))
            point->color = ft_strtol_base(comma + 2, 16);
        else
            point->color = ft_strtol(comma, NULL);
        point->has_color = 1;
    }
    else
    {
        point->color = DEFAULT_COLOR;
        point->has_color = 0;
    }

    return (1);
}

/*
 * ft_strtol - 文字列を長整数に変換
 *
 * オーバーフロー検出付き
 */
long ft_strtol(const char *str, char **endptr)
{
    long    result = 0;
    int     sign = 1;
    int     i = 0;

    /* 空白スキップ */
    while (str[i] == ' ' || (str[i] >= '\t' && str[i] <= '\r'))
        i++;

    /* 符号処理 */
    if (str[i] == '-' || str[i] == '+')
    {
        if (str[i] == '-')
            sign = -1;
        i++;
    }

    /* 数値変換 */
    while (str[i] >= '0' && str[i] <= '9')
    {
        /* オーバーフローチェック */
        if (result > (LONG_MAX - (str[i] - '0')) / 10)
        {
            if (endptr)
                *endptr = (char *)&str[i];
            return (sign == 1 ? LONG_MAX : LONG_MIN);
        }
        result = result * 10 + (str[i] - '0');
        i++;
    }

    if (endptr)
        *endptr = (char *)&str[i];

    return (result * sign);
}

3.3.2 16進数パース

/*
 * ft_strtol_base - 任意の基数で文字列を変換
 */
long ft_strtol_base(const char *str, int base)
{
    long    result = 0;
    int     digit;
    int     i = 0;

    while (str[i])
    {
        if (str[i] >= '0' && str[i] <= '9')
            digit = str[i] - '0';
        else if (str[i] >= 'a' && str[i] <= 'f')
            digit = str[i] - 'a' + 10;
        else if (str[i] >= 'A' && str[i] <= 'F')
            digit = str[i] - 'A' + 10;
        else
            break ;

        if (digit >= base)
            break ;

        result = result * base + digit;
        i++;
    }

    return (result);
}

/*
 * 使用例:
 *
 * "FF0000" を基数16でパース:
 * F(15) * 16^5 + F(15) * 16^4 + 0 * 16^3 + 0 * 16^2 + 0 * 16 + 0
 * = 16711680 (0xFF0000)
 */

3.4 行のパース

3.4.1 ft_splitによるトークン分割

/*
 * parse_line - 1行をパースして点の配列を作成
 *
 * @line: 入力行(例: "0 10 20,0xFF0000 10 0")
 * @width: 期待される列数(0なら任意)
 * @points: 結果を格納する配列
 * @return: 実際の列数(エラー時は-1)
 */
int parse_line(const char *line, int expected_width, t_point *points)
{
    char    **tokens;
    int     count;
    int     i;

    /* トークン分割 */
    tokens = ft_split(line, ' ');
    if (!tokens)
        return (-1);

    /* トークン数をカウント */
    count = 0;
    while (tokens[count] && tokens[count][0] != '\n')
        count++;

    /* 幅の一貫性チェック */
    if (expected_width > 0 && count != expected_width)
    {
        free_tokens(tokens);
        return (-1);
    }

    /* 各トークンをパース */
    i = 0;
    while (i < count)
    {
        if (!parse_point(tokens[i], &points[i]))
        {
            free_tokens(tokens);
            return (-1);
        }
        points[i].x = i;  /* 列番号を設定 */
        i++;
    }

    free_tokens(tokens);
    return (count);
}

3.4.2 空白処理の注意点

/*
 * 空白文字の処理パターン
 *
 * 入力例1: "0 10 20 30"     → 正常
 * 入力例2: "  0 10 20 30 "  → 前後の空白を処理
 * 入力例3: "0  10   20"     → 連続空白を処理
 * 入力例4: "0\t10\t20"      → タブを処理
 * 入力例5: "0 10 20\n"      → 改行を処理
 */

/*
 * ft_split_whitespace - 任意の空白文字で分割
 */
char **ft_split_whitespace(const char *s)
{
    /* 空白文字: スペース、タブ、改行など */
    return (ft_split_set(s, " \t\n\v\f\r"));
}

/*
 * trim_line - 行の前後の空白と改行を除去
 */
char *trim_line(const char *line)
{
    int     start;
    int     end;
    char    *trimmed;

    start = 0;
    while (line[start] && ft_isspace(line[start]))
        start++;

    end = ft_strlen(line) - 1;
    while (end > start && ft_isspace(line[end]))
        end--;

    trimmed = ft_substr(line, start, end - start + 1);
    return (trimmed);
}

3.5 マップデータ構造

3.5.1 2次元配列の管理

/*
 * マップのメモリレイアウト
 *
 * t_map構造体:
 * ┌────────────────────────────────────────┐
 * │ width: 10                              │
 * │ height: 5                              │
 * │ z_min: 0                               │
 * │ z_max: 20                              │
 * │ points: ─────────┐                     │
 * └──────────────────│─────────────────────┘
 *                    ↓
 *            ┌───────────────┐
 *            │ points[0] ─────→ [P0,0][P1,0][P2,0]...
 *            │ points[1] ─────→ [P0,1][P1,1][P2,1]...
 *            │ points[2] ─────→ [P0,2][P1,2][P2,2]...
 *            │ ...            │
 *            └───────────────┘
 */

typedef struct s_map
{
    int         width;      /* マップの幅(列数) */
    int         height;     /* マップの高さ(行数) */
    int         z_min;      /* 最小高度 */
    int         z_max;      /* 最大高度 */
    t_point     **points;   /* 2次元配列へのポインタ */
}   t_map;

/*
 * allocate_map - マップ用メモリを確保
 */
t_map *allocate_map(int width, int height)
{
    t_map   *map;
    int     i;

    map = malloc(sizeof(t_map));
    if (!map)
        return (NULL);

    map->width = width;
    map->height = height;
    map->z_min = INT_MAX;
    map->z_max = INT_MIN;

    /* 行ポインタの配列を確保 */
    map->points = malloc(sizeof(t_point *) * height);
    if (!map->points)
    {
        free(map);
        return (NULL);
    }

    /* 各行のメモリを確保 */
    i = 0;
    while (i < height)
    {
        map->points[i] = malloc(sizeof(t_point) * width);
        if (!map->points[i])
        {
            /* 確保済みメモリを解放 */
            while (--i >= 0)
                free(map->points[i]);
            free(map->points);
            free(map);
            return (NULL);
        }
        i++;
    }

    return (map);
}

3.5.2 マップの解放

/*
 * free_map - マップのメモリを解放
 *
 * メモリリークを防ぐため、確保した逆順で解放
 */
void free_map(t_map *map)
{
    int i;

    if (!map)
        return ;

    /* 各行を解放 */
    if (map->points)
    {
        i = 0;
        while (i < map->height)
        {
            if (map->points[i])
                free(map->points[i]);
            i++;
        }
        /* 行ポインタ配列を解放 */
        free(map->points);
    }

    /* マップ構造体を解放 */
    free(map);
}

/*
 * 解放の順序:
 *
 * 確保: map → points配列 → points[0] → points[1] → ...
 * 解放: ... → points[1] → points[0] → points配列 → map
 *
 * 理由: 親ポインタを先に解放すると子にアクセスできなくなる
 */

3.6 完全なパース実装

3.6.1 2パス方式

/*
 * 2パス方式のパース戦略:
 *
 * パス1: ファイルをスキャンして寸法を取得
 *        ├── 行数をカウント(height)
 *        └── 各行の列数を確認(width)
 *
 * パス2: データを実際にパース
 *        ├── メモリを確保
 *        └── 各点のデータを格納
 */

/*
 * パス1: 寸法の取得
 */
int get_dimensions(t_line *lines, int *width, int *height)
{
    t_line  *current;
    int     line_width;
    int     first_width;

    *height = 0;
    first_width = -1;
    current = lines;

    while (current)
    {
        line_width = count_tokens(current->content);
        if (line_width == 0)
        {
            current = current->next;
            continue ;  /* 空行はスキップ */
        }

        if (first_width == -1)
            first_width = line_width;
        else if (line_width != first_width)
            return (error_return("Inconsistent row width", 0));

        (*height)++;
        current = current->next;
    }

    *width = first_width;

    if (*width <= 0 || *height <= 0)
        return (error_return("Invalid map dimensions", 0));

    return (1);
}

3.6.2 メインパース関数

/*
 * parse_map - マップファイルを完全にパース
 */
t_map *parse_map(const char *filename)
{
    t_line  *lines;
    t_map   *map;
    int     width, height;

    /* ファイル検証 */
    if (!validate_file(filename))
        return (NULL);

    /* ファイル読み込み */
    lines = read_file(filename);
    if (!lines)
        return (NULL);

    /* パス1: 寸法取得 */
    if (!get_dimensions(lines, &width, &height))
    {
        free_lines(lines);
        return (NULL);
    }

    /* メモリ確保 */
    map = allocate_map(width, height);
    if (!map)
    {
        free_lines(lines);
        return (NULL);
    }

    /* パス2: データパース */
    if (!fill_map(lines, map))
    {
        free_map(map);
        free_lines(lines);
        return (NULL);
    }

    /* クリーンアップ */
    free_lines(lines);

    /* 統計計算 */
    calculate_z_range(map);

    return (map);
}

/*
 * fill_map - マップにデータを充填
 */
int fill_map(t_line *lines, t_map *map)
{
    t_line  *current;
    int     y;
    int     parsed_width;

    current = lines;
    y = 0;

    while (current && y < map->height)
    {
        /* 空行スキップ */
        if (is_empty_line(current->content))
        {
            current = current->next;
            continue ;
        }

        /* 行をパース */
        parsed_width = parse_line(current->content, map->width, map->points[y]);
        if (parsed_width != map->width)
            return (0);

        /* Y座標を設定 */
        set_y_coordinates(map->points[y], y, map->width);

        y++;
        current = current->next;
    }

    return (y == map->height);
}

3.6.3 統計計算

/*
 * calculate_z_range - Z値の範囲を計算
 *
 * 色のグラデーション計算に使用
 */
void calculate_z_range(t_map *map)
{
    int x, y;
    int z;

    map->z_min = INT_MAX;
    map->z_max = INT_MIN;

    y = 0;
    while (y < map->height)
    {
        x = 0;
        while (x < map->width)
        {
            z = map->points[y][x].z;
            if (z < map->z_min)
                map->z_min = z;
            if (z > map->z_max)
                map->z_max = z;
            x++;
        }
        y++;
    }

    /* 平坦なマップの場合 */
    if (map->z_min == map->z_max)
        map->z_max = map->z_min + 1;
}

3.7 エラー処理のベストプラクティス

3.7.1 エラーコードとメッセージ

/*
 * エラー処理の設計パターン
 */

/* エラーコード定義 */
typedef enum e_error
{
    ERR_NONE = 0,
    ERR_FILE_NOT_FOUND,
    ERR_FILE_PERMISSION,
    ERR_INVALID_FORMAT,
    ERR_MEMORY,
    ERR_INVALID_DATA
}   t_error;

/* エラーメッセージ配列 */
static const char *g_error_messages[] = {
    "No error",
    "File not found",
    "Permission denied",
    "Invalid file format",
    "Memory allocation failed",
    "Invalid data in file"
};

/*
 * error_exit - エラーメッセージを表示して終了
 */
int error_exit(t_error code)
{
    ft_putstr_fd("Error: ", 2);
    ft_putendl_fd(g_error_messages[code], 2);
    return (1);
}

/*
 * error_null - エラーメッセージを表示してNULL返却
 */
void *error_null(const char *msg)
{
    ft_putstr_fd("Error: ", 2);
    ft_putendl_fd(msg, 2);
    return (NULL);
}

3.7.2 パースエラーの詳細情報

/*
 * 詳細なエラー報告
 */
typedef struct s_parse_error
{
    int         line_number;
    int         column;
    const char  *message;
    const char  *context;
}   t_parse_error;

void report_parse_error(t_parse_error *err)
{
    ft_putstr_fd("Parse error at line ", 2);
    ft_putnbr_fd(err->line_number, 2);
    ft_putstr_fd(", column ", 2);
    ft_putnbr_fd(err->column, 2);
    ft_putstr_fd(": ", 2);
    ft_putendl_fd(err->message, 2);

    if (err->context)
    {
        ft_putstr_fd("  → ", 2);
        ft_putendl_fd(err->context, 2);
    }
}

/*
 * 使用例:
 * Parse error at line 5, column 12: Invalid number format
 *   → "0 10 abc 20"
 *             ^^^
 */

3.8 テストとデバッグ

3.8.1 テスト用ヘルパー関数

/*
 * print_map - マップデータをデバッグ出力
 */
void print_map(t_map *map)
{
    int x, y;

    printf("Map: %d x %d\n", map->width, map->height);
    printf("Z range: %d to %d\n", map->z_min, map->z_max);
    printf("\n");

    y = 0;
    while (y < map->height)
    {
        x = 0;
        while (x < map->width)
        {
            printf("%4d", map->points[y][x].z);
            if (map->points[y][x].has_color)
                printf(",#%06X", map->points[y][x].color);
            printf(" ");
            x++;
        }
        printf("\n");
        y++;
    }
}

3.8.2 テストケース

テストファイル例:

1. minimal.fdf - 最小マップ
   0

2. simple.fdf - シンプルな3x3
   0 0 0
   0 1 0
   0 0 0

3. pyramid.fdf - ピラミッド形状
   0 0 0 0 0
   0 1 1 1 0
   0 1 2 1 0
   0 1 1 1 0
   0 0 0 0 0

4. colored.fdf - 色付き
   0,0xFF0000 0 0
   0 10,0x00FF00 0
   0 0 0,0x0000FF

5. negative.fdf - 負の高さ
   -5 0 5
   0 10 0
   5 0 -5

6. large.fdf - 大きなマップ(パフォーマンステスト用)
   [100x100 の数値]

7. edge_cases/
   ├── empty.fdf          - 空ファイル(エラー)
   ├── single_row.fdf     - 1行のみ
   ├── single_col.fdf     - 1列のみ
   ├── inconsistent.fdf   - 行幅不一致(エラー)
   └── overflow.fdf       - 極端に大きな数値

3.9 本章のまとめ

パース処理のフローチャート:

┌───────────────────────────────────────────────────────┐
│                   parse_map()                         │
├───────────────────────────────────────────────────────┤
│                                                       │
│  ┌─────────────┐     ┌─────────────┐                 │
│  │ファイル検証 │ ──→ │ファイル読込 │                 │
│  └─────────────┘     └─────────────┘                 │
│         │                    │                        │
│         ↓                    ↓                        │
│  ┌─────────────┐     ┌─────────────┐                 │
│  │  エラー?    │     │ 行をリストに│                 │
│  │   Yes → 終了│     │    格納     │                 │
│  └─────────────┘     └─────────────┘                 │
│                              │                        │
│                              ↓                        │
│  ┌──────────────────────────────────────────┐        │
│  │           パス1: 寸法取得                 │        │
│  │  - 行数カウント (height)                 │        │
│  │  - 各行の列数確認 (width)                │        │
│  │  - 一貫性チェック                        │        │
│  └──────────────────────────────────────────┘        │
│                              │                        │
│                              ↓                        │
│  ┌──────────────────────────────────────────┐        │
│  │           メモリ確保                      │        │
│  │  - t_map構造体                           │        │
│  │  - points[height]配列                    │        │
│  │  - 各行 points[y][width]                 │        │
│  └──────────────────────────────────────────┘        │
│                              │                        │
│                              ↓                        │
│  ┌──────────────────────────────────────────┐        │
│  │           パス2: データパース             │        │
│  │  - 各行をトークン分割                    │        │
│  │  - 各トークンをt_pointに変換             │        │
│  │  - Z値、色、座標を設定                   │        │
│  └──────────────────────────────────────────┘        │
│                              │                        │
│                              ↓                        │
│  ┌──────────────────────────────────────────┐        │
│  │           統計計算                        │        │
│  │  - z_min, z_max の計算                   │        │
│  └──────────────────────────────────────────┘        │
│                              │                        │
│                              ↓                        │
│  ┌─────────────┐                                     │
│  │ return map; │                                     │
│  └─────────────┘                                     │
└───────────────────────────────────────────────────────┘

重要ポイント:

  • 2パス方式: 寸法確認後にメモリ確保で効率的
  • 一貫性チェック: 全行の幅が同じであることを検証
  • メモリ管理: 確保と解放を対にして漏れを防ぐ
  • エラー処理: 詳細なエラーメッセージで問題を特定しやすく
  • 色情報: オプショナルな色データにも対応

次章では、座標変換と投影の実装について詳しく学びます。