using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
/// One row of cases.csv.
public class CaseDefinition
{
public int Number { get; set; } // Number
public string Type { get; set; } // Type 心音 / 呼吸音
public string CategoryJp { get; set; } // Category
public string SubcategoryJp { get; set; } // Subcategory
public string LocationJp { get; set; } // Location
public string TreeLevel1 { get; set; } // Tree_Level1
public string TreeLevel2 { get; set; } // Tree_Level2
public string TreeLevel3 { get; set; } // Tree_Level3
public string MapFront { get; set; } // Image_File
public string SoundPath { get; set; } // Sound_File
public string MapRight { get; set; } // Image_Right
public string MapLeft { get; set; } // Image_Left
public string MapBack { get; set; } // Image_Back
public bool IsHeart
{
get { return !string.IsNullOrEmpty(Type) && Type.Contains("心"); }
}
/// Non-empty levels only, e.g. 呼吸音 → 正常呼吸音 → 気管音.
/// Depth varies per row, which is what drives the drill-down.
public string[] TreePath
{
get
{
var parts = new List<string>();
foreach (string s in new[] { Type, TreeLevel1, TreeLevel2, TreeLevel3 })
if (!string.IsNullOrWhiteSpace(s)) parts.Add(s.Trim());
return parts.ToArray();
}
}
public override string ToString()
{
return Number + ": " + string.Join(" : ", TreePath);
}
// ── loader ─────────────────────────────────────────────────────────
public static List<CaseDefinition> LoadCasesFromCsv(string path)
{
var list = new List<CaseDefinition>();
if (!File.Exists(path))
{
Console.WriteLine("CSV not found: " + path);
return list;
}
// Encoding.UTF8 with BOM detection. Reading Japanese as the default
// ANSI codepage gives mojibake that only shows up on the buttons.
string[] lines;
try
{
lines = File.ReadAllLines(path, Encoding.UTF8);
}
catch (Exception ex)
{
Console.WriteLine("Failed to read " + path + ": " + ex.Message);
return list;
}
if (lines.Length < 2)
{
Console.WriteLine("CSV has no data rows: " + path);
return list;
}
// Your file is TAB separated; fall back to comma if it gets re-exported.
char sep = lines[0].Contains("\t") ? '\t' : ',';
// Header name -> column index, so adding or reordering columns is safe.
// string[] header = lines[0].TrimStart('\uFEFF').Split(sep);
string[] header = lines[0].TrimStart(new[] { '\uFEFF' }).Split(sep);
var idx = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
for (int i = 0; i < header.Length; i++)
{
string key = header[i].Trim();
if (key.Length > 0 && !idx.ContainsKey(key)) idx[key] = i;
}
if (!idx.ContainsKey("Number"))
{
Console.WriteLine("CSV header has no 'Number' column — wrong file or wrong delimiter?");
return list;
}
Func<string[], string, string> get = (fields, name) =>
{
int i;
if (!idx.TryGetValue(name, out i) || i >= fields.Length) return "";
return fields[i].Trim();
};
int skipped = 0;
for (int r = 1; r < lines.Length; r++)
{
if (string.IsNullOrWhiteSpace(lines[r])) continue;
string[] f = lines[r].Split(sep);
int number;
if (!int.TryParse(get(f, "Number"), NumberStyles.Integer,
CultureInfo.InvariantCulture, out number))
{
skipped++;
Console.WriteLine("Line " + (r + 1) + ": bad Number, skipped");
continue;
}
list.Add(new CaseDefinition
{
Number = number,
Type = get(f, "Type"),
CategoryJp = get(f, "Category"),
SubcategoryJp = get(f, "Subcategory"),
LocationJp = get(f, "Location"),
TreeLevel1 = get(f, "Tree_Level1"),
TreeLevel2 = get(f, "Tree_Level2"),
TreeLevel3 = get(f, "Tree_Level3"),
MapFront = get(f, "Image_File"),
SoundPath = get(f, "Sound_File"),
MapRight = get(f, "Image_Right"),
MapLeft = get(f, "Image_Left"),
MapBack = get(f, "Image_Back")
});
}
Console.WriteLine("Loaded " + list.Count + " cases from " + path +
(skipped > 0 ? " (" + skipped + " skipped)" : ""));
return list;
}
}