第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パス方式: 寸法確認後にメモリ確保で効率的
- 一貫性チェック: 全行の幅が同じであることを検証
- メモリ管理: 確保と解放を対にして漏れを防ぐ
- エラー処理: 詳細なエラーメッセージで問題を特定しやすく
- 色情報: オプショナルな色データにも対応
次章では、座標変換と投影の実装について詳しく学びます。