rbcsv 0.1.7 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +38 -0
- data/Cargo.lock +1 -1
- data/DEVELOPMENT.md +259 -55
- data/README.md +103 -20
- data/docs/exe_upgrade_version.md +124 -0
- data/docs/release_process_v0.1.8.md +298 -0
- data/docs/special_character_bug_fix.md +257 -0
- data/docs/write_functionality_implementation.md +197 -0
- data/examples/README.md +221 -0
- data/{test.rb → examples/basic/basic_usage.rb} +2 -1
- data/{test_fixed.rb → examples/basic/test_fixed.rb} +1 -1
- data/examples/benchmarks/benchmark.rb +372 -0
- data/{output_comparison.rb → examples/benchmarks/output_comparison.rb} +41 -26
- data/examples/benchmarks/sample.csv +1001 -0
- data/examples/features/test_typed_functionality.rb +109 -0
- data/examples/features/test_write_functionality.rb +225 -0
- data/ext/rbcsv/Cargo.toml +1 -1
- data/ext/rbcsv/src/error.rs +16 -2
- data/ext/rbcsv/src/lib.rs +9 -1
- data/ext/rbcsv/src/parser.rs +202 -24
- data/ext/rbcsv/src/ruby_api.rs +115 -3
- data/ext/rbcsv/src/value.rs +87 -0
- data/lib/rbcsv/version.rb +1 -1
- metadata +16 -7
- data/benchmark.rb +0 -190
- /data/{quick_test.rb → examples/basic/quick_test.rb} +0 -0
- /data/{test_install.rb → examples/basic/test_install.rb} +0 -0
data/ext/rbcsv/src/parser.rs
CHANGED
|
@@ -2,13 +2,10 @@ use crate::error::{CsvError, ErrorKind};
|
|
|
2
2
|
use std::fs;
|
|
3
3
|
use std::path::Path;
|
|
4
4
|
|
|
5
|
-
/// CSV解析のオプション設定
|
|
6
5
|
#[derive(Debug, Clone)]
|
|
6
|
+
#[allow(dead_code)]
|
|
7
7
|
pub struct CsvParseOptions {
|
|
8
8
|
pub trim: bool,
|
|
9
|
-
// 将来的な拡張用
|
|
10
|
-
// pub headers: bool,
|
|
11
|
-
// pub delimiter: char,
|
|
12
9
|
}
|
|
13
10
|
|
|
14
11
|
impl Default for CsvParseOptions {
|
|
@@ -19,8 +16,7 @@ impl Default for CsvParseOptions {
|
|
|
19
16
|
}
|
|
20
17
|
}
|
|
21
18
|
|
|
22
|
-
|
|
23
|
-
pub fn escape_sanitize(s: &str) -> String {
|
|
19
|
+
pub fn _escape_sanitize(s: &str) -> String {
|
|
24
20
|
s.replace("\\n", "\n")
|
|
25
21
|
.replace("\\r", "\r")
|
|
26
22
|
.replace("\\t", "\t")
|
|
@@ -30,18 +26,15 @@ pub fn escape_sanitize(s: &str) -> String {
|
|
|
30
26
|
|
|
31
27
|
/// 基本的なCSVパース処理
|
|
32
28
|
pub fn parse_csv_core(input: &str, trim_config: csv::Trim) -> Result<Vec<Vec<String>>, CsvError> {
|
|
33
|
-
|
|
29
|
+
println!("input: {:?}", input);
|
|
34
30
|
if input.trim().is_empty() {
|
|
35
31
|
return Err(CsvError::empty_data());
|
|
36
32
|
}
|
|
37
33
|
|
|
38
|
-
// エスケープシーケンスを実際の文字に変換
|
|
39
|
-
let processed = escape_sanitize(input);
|
|
40
|
-
|
|
41
34
|
let mut reader = csv::ReaderBuilder::new()
|
|
42
|
-
.has_headers(false)
|
|
35
|
+
.has_headers(false)
|
|
43
36
|
.trim(trim_config)
|
|
44
|
-
.from_reader(
|
|
37
|
+
.from_reader(input.as_bytes());
|
|
45
38
|
|
|
46
39
|
let mut records = Vec::new();
|
|
47
40
|
|
|
@@ -52,7 +45,6 @@ pub fn parse_csv_core(input: &str, trim_config: csv::Trim) -> Result<Vec<Vec<Str
|
|
|
52
45
|
records.push(row);
|
|
53
46
|
}
|
|
54
47
|
Err(e) => {
|
|
55
|
-
// フィールド数不一致エラーを詳細化
|
|
56
48
|
if let csv::ErrorKind::UnequalLengths { expected_len, len, .. } = e.kind() {
|
|
57
49
|
let error_msg = format!(
|
|
58
50
|
"Field count mismatch at line {}: expected {} fields, got {} fields",
|
|
@@ -63,7 +55,6 @@ pub fn parse_csv_core(input: &str, trim_config: csv::Trim) -> Result<Vec<Vec<Str
|
|
|
63
55
|
return Err(CsvError::new(ErrorKind::FieldCountMismatch, error_msg));
|
|
64
56
|
}
|
|
65
57
|
|
|
66
|
-
// その他のcsvエラーを自動変換
|
|
67
58
|
return Err(CsvError::from(e));
|
|
68
59
|
}
|
|
69
60
|
}
|
|
@@ -77,13 +68,13 @@ pub fn parse_csv_core(input: &str, trim_config: csv::Trim) -> Result<Vec<Vec<Str
|
|
|
77
68
|
}
|
|
78
69
|
|
|
79
70
|
/// オプション設定を使ったCSV解析(文字列用)
|
|
80
|
-
pub fn
|
|
71
|
+
pub fn _parse_csv_with_options(input: &str, options: &CsvParseOptions) -> Result<Vec<Vec<String>>, CsvError> {
|
|
81
72
|
let trim_config = if options.trim { csv::Trim::All } else { csv::Trim::None };
|
|
82
73
|
parse_csv_core(input, trim_config)
|
|
83
74
|
}
|
|
84
75
|
|
|
85
76
|
/// オプション設定を使ったCSV解析(ファイル用)
|
|
86
|
-
pub fn
|
|
77
|
+
pub fn _parse_csv_file_with_options(file_path: &str, options: &CsvParseOptions) -> Result<Vec<Vec<String>>, CsvError> {
|
|
87
78
|
let trim_config = if options.trim { csv::Trim::All } else { csv::Trim::None };
|
|
88
79
|
parse_csv_file(file_path, trim_config)
|
|
89
80
|
}
|
|
@@ -112,17 +103,81 @@ pub fn parse_csv_file(file_path: &str, trim_config: csv::Trim) -> Result<Vec<Vec
|
|
|
112
103
|
parse_csv_core(&content, trim_config)
|
|
113
104
|
}
|
|
114
105
|
|
|
106
|
+
/// 型認識を行うCSVパース処理
|
|
107
|
+
pub fn parse_csv_typed(input: &str, trim_config: csv::Trim) -> Result<Vec<Vec<crate::value::CsvValue>>, CsvError> {
|
|
108
|
+
use crate::value::CsvValue;
|
|
109
|
+
|
|
110
|
+
if input.trim().is_empty() {
|
|
111
|
+
return Err(CsvError::empty_data());
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let mut reader = csv::ReaderBuilder::new()
|
|
115
|
+
.has_headers(false)
|
|
116
|
+
.trim(trim_config)
|
|
117
|
+
.from_reader(input.as_bytes());
|
|
118
|
+
|
|
119
|
+
let mut records = Vec::new();
|
|
120
|
+
|
|
121
|
+
for (line_num, result) in reader.records().enumerate() {
|
|
122
|
+
match result {
|
|
123
|
+
Ok(record) => {
|
|
124
|
+
let row: Vec<CsvValue> = record.iter().map(|field| {
|
|
125
|
+
if matches!(trim_config, csv::Trim::All | csv::Trim::Fields) {
|
|
126
|
+
CsvValue::from_str_trimmed(field)
|
|
127
|
+
} else {
|
|
128
|
+
CsvValue::from_str(field)
|
|
129
|
+
}
|
|
130
|
+
}).collect();
|
|
131
|
+
records.push(row);
|
|
132
|
+
}
|
|
133
|
+
Err(e) => {
|
|
134
|
+
if let csv::ErrorKind::UnequalLengths { expected_len, len, .. } = e.kind() {
|
|
135
|
+
let error_msg = format!(
|
|
136
|
+
"Field count mismatch at line {}: expected {} fields, got {} fields",
|
|
137
|
+
line_num + 1,
|
|
138
|
+
expected_len,
|
|
139
|
+
len
|
|
140
|
+
);
|
|
141
|
+
return Err(CsvError::new(ErrorKind::FieldCountMismatch, error_msg));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return Err(CsvError::from(e));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if records.is_empty() {
|
|
150
|
+
return Err(CsvError::empty_data());
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
Ok(records)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/// 型認識を行うCSVファイル読み込み処理
|
|
157
|
+
pub fn parse_csv_file_typed(file_path: &str, trim_config: csv::Trim) -> Result<Vec<Vec<crate::value::CsvValue>>, CsvError> {
|
|
158
|
+
let path = Path::new(file_path);
|
|
159
|
+
if !path.exists() {
|
|
160
|
+
return Err(CsvError::io(format!("File not found: {}", file_path)));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if !path.is_file() {
|
|
164
|
+
return Err(CsvError::io(format!("Path is not a file: {}", file_path)));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let content = match fs::read_to_string(path) {
|
|
168
|
+
Ok(content) => content,
|
|
169
|
+
Err(e) => {
|
|
170
|
+
return Err(CsvError::io(format!("Failed to read file '{}': {}", file_path, e)));
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
parse_csv_typed(&content, trim_config)
|
|
175
|
+
}
|
|
176
|
+
|
|
115
177
|
#[cfg(test)]
|
|
116
178
|
mod tests {
|
|
117
179
|
use super::*;
|
|
118
180
|
|
|
119
|
-
#[test]
|
|
120
|
-
fn test_escape_sanitize() {
|
|
121
|
-
let input = "Hello\\nWorld\\t\\\"Test\\\"\\\\End";
|
|
122
|
-
let expected = "Hello\nWorld\t\"Test\"\\End";
|
|
123
|
-
assert_eq!(escape_sanitize(input), expected);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
181
|
#[test]
|
|
127
182
|
fn test_parse_csv_core_basic() {
|
|
128
183
|
let csv_data = "a,b,c\n1,2,3";
|
|
@@ -184,4 +239,127 @@ mod tests {
|
|
|
184
239
|
assert_eq!(records[1], vec!["Alice", "25", "Tokyo"]);
|
|
185
240
|
assert_eq!(records[2], vec!["Bob", "30", "Osaka"]);
|
|
186
241
|
}
|
|
187
|
-
|
|
242
|
+
|
|
243
|
+
#[test]
|
|
244
|
+
fn test_write_csv_file_basic() {
|
|
245
|
+
|
|
246
|
+
let temp_path = "/tmp/test_write_csv.csv";
|
|
247
|
+
let test_data = vec![
|
|
248
|
+
vec!["name".to_string(), "age".to_string(), "city".to_string()],
|
|
249
|
+
vec!["Alice".to_string(), "25".to_string(), "Tokyo".to_string()],
|
|
250
|
+
vec!["Bob".to_string(), "30".to_string(), "Osaka".to_string()],
|
|
251
|
+
];
|
|
252
|
+
|
|
253
|
+
// ファイルに書き込み
|
|
254
|
+
let result = write_csv_file(temp_path, &test_data);
|
|
255
|
+
assert!(result.is_ok(), "Write should succeed");
|
|
256
|
+
|
|
257
|
+
// 書き込んだファイルを読み込んで検証
|
|
258
|
+
let content = std::fs::read_to_string(temp_path).expect("Failed to read written file");
|
|
259
|
+
let expected = "name,age,city\nAlice,25,Tokyo\nBob,30,Osaka\n";
|
|
260
|
+
assert_eq!(content, expected);
|
|
261
|
+
|
|
262
|
+
// クリーンアップ
|
|
263
|
+
let _ = std::fs::remove_file(temp_path);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
#[test]
|
|
267
|
+
fn test_write_csv_file_empty_data() {
|
|
268
|
+
let temp_path = "/tmp/test_write_empty.csv";
|
|
269
|
+
let empty_data: Vec<Vec<String>> = vec![];
|
|
270
|
+
|
|
271
|
+
let result = write_csv_file(temp_path, &empty_data);
|
|
272
|
+
assert!(result.is_err());
|
|
273
|
+
if let Err(e) = result {
|
|
274
|
+
assert!(e.to_string().contains("CSV data is empty"));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
#[test]
|
|
279
|
+
fn test_write_csv_file_field_count_mismatch() {
|
|
280
|
+
let temp_path = "/tmp/test_write_mismatch.csv";
|
|
281
|
+
let inconsistent_data = vec![
|
|
282
|
+
vec!["name".to_string(), "age".to_string()],
|
|
283
|
+
vec!["Alice".to_string(), "25".to_string(), "Tokyo".to_string()], // 3 fields instead of 2
|
|
284
|
+
];
|
|
285
|
+
|
|
286
|
+
let result = write_csv_file(temp_path, &inconsistent_data);
|
|
287
|
+
assert!(result.is_err());
|
|
288
|
+
if let Err(e) = result {
|
|
289
|
+
assert!(e.to_string().contains("Field count mismatch"));
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
#[test]
|
|
294
|
+
fn test_write_csv_file_permission_denied() {
|
|
295
|
+
// 書き込み権限のないパスをテスト(rootディレクトリ)
|
|
296
|
+
let result = write_csv_file("/root/test.csv", &vec![vec!["test".to_string()]]);
|
|
297
|
+
assert!(result.is_err());
|
|
298
|
+
if let Err(e) = result {
|
|
299
|
+
// Permission deniedまたはParent directory does not existのいずれかになる
|
|
300
|
+
let error_msg = e.to_string();
|
|
301
|
+
assert!(error_msg.contains("Permission denied") || error_msg.contains("Parent directory does not exist"));
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/// CSVデータをファイルに書き込む
|
|
307
|
+
pub fn write_csv_file(file_path: &str, data: &[Vec<String>]) -> Result<(), CsvError> {
|
|
308
|
+
// データ検証:空配列チェック
|
|
309
|
+
if data.is_empty() {
|
|
310
|
+
return Err(CsvError::invalid_data("CSV data is empty"));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// データ検証:各行のフィールド数一貫性チェック
|
|
314
|
+
if data.len() > 1 {
|
|
315
|
+
let expected_len = data[0].len();
|
|
316
|
+
for (line_num, row) in data.iter().enumerate() {
|
|
317
|
+
if row.len() != expected_len {
|
|
318
|
+
let error_msg = format!(
|
|
319
|
+
"Field count mismatch at line {}: expected {} fields, got {} fields",
|
|
320
|
+
line_num + 1,
|
|
321
|
+
expected_len,
|
|
322
|
+
row.len()
|
|
323
|
+
);
|
|
324
|
+
return Err(CsvError::invalid_data(error_msg));
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ファイルパス検証:親ディレクトリの存在確認
|
|
330
|
+
let path = Path::new(file_path);
|
|
331
|
+
if let Some(parent) = path.parent() {
|
|
332
|
+
if !parent.exists() {
|
|
333
|
+
return Err(CsvError::io(format!("Parent directory does not exist: {}", parent.display())));
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// CSV Writer作成とデータ書き込み
|
|
338
|
+
let file = match fs::File::create(path) {
|
|
339
|
+
Ok(file) => file,
|
|
340
|
+
Err(e) => {
|
|
341
|
+
if e.kind() == std::io::ErrorKind::PermissionDenied {
|
|
342
|
+
return Err(CsvError::write_permission(format!("Permission denied: {}", file_path)));
|
|
343
|
+
}
|
|
344
|
+
return Err(CsvError::io(format!("Failed to create file '{}': {}", file_path, e)));
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
let mut writer = csv::WriterBuilder::new()
|
|
349
|
+
.has_headers(false)
|
|
350
|
+
.from_writer(file);
|
|
351
|
+
|
|
352
|
+
// データ書き込み
|
|
353
|
+
for row in data {
|
|
354
|
+
if let Err(e) = writer.write_record(row) {
|
|
355
|
+
return Err(CsvError::from(e));
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ファイルフラッシュ:データの確実な書き込み保証
|
|
360
|
+
if let Err(e) = writer.flush() {
|
|
361
|
+
return Err(CsvError::io(format!("Failed to flush data to file '{}': {}", file_path, e)));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
Ok(())
|
|
365
|
+
}
|
data/ext/rbcsv/src/ruby_api.rs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
use magnus::{Error as MagnusError, Ruby};
|
|
2
|
-
use crate::parser::{parse_csv_core, parse_csv_file};
|
|
1
|
+
use magnus::{Error as MagnusError, Ruby, Value as MagnusValue, value::ReprValue};
|
|
2
|
+
use crate::parser::{parse_csv_core, parse_csv_file, write_csv_file, parse_csv_typed, parse_csv_file_typed};
|
|
3
3
|
|
|
4
4
|
/// CSV文字列をパースする(通常版)
|
|
5
5
|
///
|
|
@@ -53,10 +53,122 @@ pub fn read_trim(ruby: &Ruby, file_path: String) -> Result<Vec<Vec<String>>, Mag
|
|
|
53
53
|
.map_err(|e| MagnusError::new(ruby.exception_runtime_error(), e.to_string()))
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/// CSVファイルに書き込む
|
|
57
|
+
///
|
|
58
|
+
/// # Arguments
|
|
59
|
+
/// * `ruby` - Ruby VMの参照
|
|
60
|
+
/// * `file_path` - 書き込み先ファイルのパス
|
|
61
|
+
/// * `data` - 書き込むCSVデータ(2次元配列)
|
|
62
|
+
///
|
|
63
|
+
/// # Returns
|
|
64
|
+
/// * `Result<(), MagnusError>` - 成功時は空、失敗時はエラー
|
|
65
|
+
pub fn write(ruby: &Ruby, file_path: String, data: Vec<Vec<String>>) -> Result<(), MagnusError> {
|
|
66
|
+
write_csv_file(&file_path, &data)
|
|
67
|
+
.map_err(|e| MagnusError::new(ruby.exception_runtime_error(), e.to_string()))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/// CSV文字列を型認識してパースする(通常版)
|
|
71
|
+
///
|
|
72
|
+
/// # Arguments
|
|
73
|
+
/// * `ruby` - Ruby VMの参照
|
|
74
|
+
/// * `s` - パースするCSV文字列
|
|
75
|
+
///
|
|
76
|
+
/// # Returns
|
|
77
|
+
/// * `Result<Vec<Vec<MagnusValue>>, MagnusError>` - パース結果(数値は数値型)またはエラー
|
|
78
|
+
pub fn parse_typed(ruby: &Ruby, s: String) -> Result<MagnusValue, MagnusError> {
|
|
79
|
+
let result = parse_csv_typed(&s, csv::Trim::None)
|
|
80
|
+
.map_err(|e| MagnusError::new(ruby.exception_runtime_error(), e.to_string()))?;
|
|
81
|
+
|
|
82
|
+
// Vec<Vec<CsvValue>> を Ruby配列に変換
|
|
83
|
+
let outer_array = ruby.ary_new();
|
|
84
|
+
for row in result {
|
|
85
|
+
let inner_array = ruby.ary_new();
|
|
86
|
+
for value in row {
|
|
87
|
+
inner_array.push(value.to_ruby(ruby))?;
|
|
88
|
+
}
|
|
89
|
+
outer_array.push(inner_array.as_value())?;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
Ok(outer_array.as_value())
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// CSV文字列を型認識してパースする(trim版)
|
|
96
|
+
///
|
|
97
|
+
/// # Arguments
|
|
98
|
+
/// * `ruby` - Ruby VMの参照
|
|
99
|
+
/// * `s` - パースするCSV文字列
|
|
100
|
+
///
|
|
101
|
+
/// # Returns
|
|
102
|
+
/// * `Result<Vec<Vec<MagnusValue>>, MagnusError>` - パース結果(数値は数値型)またはエラー
|
|
103
|
+
pub fn parse_typed_trim(ruby: &Ruby, s: String) -> Result<MagnusValue, MagnusError> {
|
|
104
|
+
let result = parse_csv_typed(&s, csv::Trim::All)
|
|
105
|
+
.map_err(|e| MagnusError::new(ruby.exception_runtime_error(), e.to_string()))?;
|
|
106
|
+
|
|
107
|
+
// Vec<Vec<CsvValue>> を Ruby配列に変換
|
|
108
|
+
let outer_array = ruby.ary_new();
|
|
109
|
+
for row in result {
|
|
110
|
+
let inner_array = ruby.ary_new();
|
|
111
|
+
for value in row {
|
|
112
|
+
inner_array.push(value.to_ruby(ruby))?;
|
|
113
|
+
}
|
|
114
|
+
outer_array.push(inner_array.as_value())?;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
Ok(outer_array.as_value())
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// CSVファイルを型認識して読み込む(通常版)
|
|
121
|
+
///
|
|
122
|
+
/// # Arguments
|
|
123
|
+
/// * `ruby` - Ruby VMの参照
|
|
124
|
+
/// * `file_path` - 読み込むCSVファイルのパス
|
|
125
|
+
///
|
|
126
|
+
/// # Returns
|
|
127
|
+
/// * `Result<Vec<Vec<MagnusValue>>, MagnusError>` - パース結果(数値は数値型)またはエラー
|
|
128
|
+
pub fn read_typed(ruby: &Ruby, file_path: String) -> Result<MagnusValue, MagnusError> {
|
|
129
|
+
let result = parse_csv_file_typed(&file_path, csv::Trim::None)
|
|
130
|
+
.map_err(|e| MagnusError::new(ruby.exception_runtime_error(), e.to_string()))?;
|
|
131
|
+
|
|
132
|
+
// Vec<Vec<CsvValue>> を Ruby配列に変換
|
|
133
|
+
let outer_array = ruby.ary_new();
|
|
134
|
+
for row in result {
|
|
135
|
+
let inner_array = ruby.ary_new();
|
|
136
|
+
for value in row {
|
|
137
|
+
inner_array.push(value.to_ruby(ruby))?;
|
|
138
|
+
}
|
|
139
|
+
outer_array.push(inner_array.as_value())?;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
Ok(outer_array.as_value())
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// CSVファイルを型認識して読み込む(trim版)
|
|
146
|
+
///
|
|
147
|
+
/// # Arguments
|
|
148
|
+
/// * `ruby` - Ruby VMの参照
|
|
149
|
+
/// * `file_path` - 読み込むCSVファイルのパス
|
|
150
|
+
///
|
|
151
|
+
/// # Returns
|
|
152
|
+
/// * `Result<Vec<Vec<MagnusValue>>, MagnusError>` - パース結果(数値は数値型)またはエラー
|
|
153
|
+
pub fn read_typed_trim(ruby: &Ruby, file_path: String) -> Result<MagnusValue, MagnusError> {
|
|
154
|
+
let result = parse_csv_file_typed(&file_path, csv::Trim::All)
|
|
155
|
+
.map_err(|e| MagnusError::new(ruby.exception_runtime_error(), e.to_string()))?;
|
|
156
|
+
|
|
157
|
+
// Vec<Vec<CsvValue>> を Ruby配列に変換
|
|
158
|
+
let outer_array = ruby.ary_new();
|
|
159
|
+
for row in result {
|
|
160
|
+
let inner_array = ruby.ary_new();
|
|
161
|
+
for value in row {
|
|
162
|
+
inner_array.push(value.to_ruby(ruby))?;
|
|
163
|
+
}
|
|
164
|
+
outer_array.push(inner_array.as_value())?;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
Ok(outer_array.as_value())
|
|
168
|
+
}
|
|
56
169
|
|
|
57
170
|
#[cfg(test)]
|
|
58
171
|
mod tests {
|
|
59
|
-
use super::*;
|
|
60
172
|
|
|
61
173
|
#[test]
|
|
62
174
|
fn test_parse_basic() {
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
use magnus::{Ruby, Value as MagnusValue, value::ReprValue};
|
|
2
|
+
|
|
3
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
4
|
+
pub enum CsvValue {
|
|
5
|
+
Integer(i64),
|
|
6
|
+
Float(f64),
|
|
7
|
+
String(String),
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
impl CsvValue {
|
|
11
|
+
/// 文字列からCsvValueへの変換
|
|
12
|
+
/// 優先順位: 整数 → 浮動小数点 → 文字列
|
|
13
|
+
pub fn from_str(s: &str) -> Self {
|
|
14
|
+
if s.is_empty() {
|
|
15
|
+
return CsvValue::String(s.to_string());
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if let Ok(i) = s.parse::<i64>() {
|
|
19
|
+
return CsvValue::Integer(i);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if let Ok(f) = s.parse::<f64>() {
|
|
23
|
+
if f.is_finite() {
|
|
24
|
+
return CsvValue::Float(f);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
CsvValue::String(s.to_string())
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
pub fn from_str_trimmed(s: &str) -> Self {
|
|
32
|
+
Self::from_str(s.trim())
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
pub fn to_ruby(&self, ruby: &Ruby) -> MagnusValue {
|
|
36
|
+
match self {
|
|
37
|
+
CsvValue::Integer(i) => ruby.integer_from_i64(*i).as_value(),
|
|
38
|
+
CsvValue::Float(f) => ruby.float_from_f64(*f).as_value(),
|
|
39
|
+
CsvValue::String(s) => ruby.str_new(s).as_value(),
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#[cfg(test)]
|
|
45
|
+
mod tests {
|
|
46
|
+
use super::*;
|
|
47
|
+
|
|
48
|
+
#[test]
|
|
49
|
+
fn test_from_str_integer() {
|
|
50
|
+
assert_eq!(CsvValue::from_str("123"), CsvValue::Integer(123));
|
|
51
|
+
assert_eq!(CsvValue::from_str("-456"), CsvValue::Integer(-456));
|
|
52
|
+
assert_eq!(CsvValue::from_str("0"), CsvValue::Integer(0));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#[test]
|
|
56
|
+
fn test_from_str_float() {
|
|
57
|
+
assert_eq!(CsvValue::from_str("123.45"), CsvValue::Float(123.45));
|
|
58
|
+
assert_eq!(CsvValue::from_str("-0.67"), CsvValue::Float(-0.67));
|
|
59
|
+
assert_eq!(CsvValue::from_str("1.23e-4"), CsvValue::Float(0.000123));
|
|
60
|
+
assert_eq!(CsvValue::from_str("3.14159"), CsvValue::Float(3.14159));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#[test]
|
|
64
|
+
fn test_from_str_string() {
|
|
65
|
+
assert_eq!(CsvValue::from_str("hello"), CsvValue::String("hello".to_string()));
|
|
66
|
+
assert_eq!(CsvValue::from_str(""), CsvValue::String("".to_string()));
|
|
67
|
+
assert_eq!(CsvValue::from_str("123abc"), CsvValue::String("123abc".to_string()));
|
|
68
|
+
assert_eq!(CsvValue::from_str("true"), CsvValue::String("true".to_string()));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
#[test]
|
|
72
|
+
fn test_from_str_edge_cases() {
|
|
73
|
+
// NaN と Infinity は文字列として扱う
|
|
74
|
+
assert_eq!(CsvValue::from_str("NaN"), CsvValue::String("NaN".to_string()));
|
|
75
|
+
assert_eq!(CsvValue::from_str("Infinity"), CsvValue::String("Infinity".to_string()));
|
|
76
|
+
|
|
77
|
+
// 非常に大きな数値(i64の範囲を超える)は浮動小数点として扱われる
|
|
78
|
+
assert!(matches!(CsvValue::from_str("99999999999999999999"), CsvValue::Float(_)));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#[test]
|
|
82
|
+
fn test_from_str_trimmed() {
|
|
83
|
+
assert_eq!(CsvValue::from_str_trimmed(" 123 "), CsvValue::Integer(123));
|
|
84
|
+
assert_eq!(CsvValue::from_str_trimmed(" 45.6 "), CsvValue::Float(45.6));
|
|
85
|
+
assert_eq!(CsvValue::from_str_trimmed(" hello "), CsvValue::String("hello".to_string()));
|
|
86
|
+
}
|
|
87
|
+
}
|
data/lib/rbcsv/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rbcsv
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- fujitani sora
|
|
@@ -48,22 +48,31 @@ files:
|
|
|
48
48
|
- LICENSE.txt
|
|
49
49
|
- README.md
|
|
50
50
|
- Rakefile
|
|
51
|
-
-
|
|
51
|
+
- docs/exe_upgrade_version.md
|
|
52
|
+
- docs/release_process_v0.1.8.md
|
|
53
|
+
- docs/special_character_bug_fix.md
|
|
54
|
+
- docs/write_functionality_implementation.md
|
|
55
|
+
- examples/README.md
|
|
56
|
+
- examples/basic/basic_usage.rb
|
|
57
|
+
- examples/basic/quick_test.rb
|
|
58
|
+
- examples/basic/test_fixed.rb
|
|
59
|
+
- examples/basic/test_install.rb
|
|
60
|
+
- examples/benchmarks/benchmark.rb
|
|
61
|
+
- examples/benchmarks/output_comparison.rb
|
|
62
|
+
- examples/benchmarks/sample.csv
|
|
63
|
+
- examples/features/test_typed_functionality.rb
|
|
64
|
+
- examples/features/test_write_functionality.rb
|
|
52
65
|
- ext/rbcsv/Cargo.toml
|
|
53
66
|
- ext/rbcsv/extconf.rb
|
|
54
67
|
- ext/rbcsv/src/error.rs
|
|
55
68
|
- ext/rbcsv/src/lib.rs
|
|
56
69
|
- ext/rbcsv/src/parser.rs
|
|
57
70
|
- ext/rbcsv/src/ruby_api.rs
|
|
71
|
+
- ext/rbcsv/src/value.rs
|
|
58
72
|
- lib/rbcsv.rb
|
|
59
73
|
- lib/rbcsv/version.rb
|
|
60
|
-
- output_comparison.rb
|
|
61
|
-
- quick_test.rb
|
|
62
74
|
- sample.csv
|
|
63
75
|
- sig/r_csv.rbs
|
|
64
|
-
- test.rb
|
|
65
|
-
- test_fixed.rb
|
|
66
|
-
- test_install.rb
|
|
67
76
|
homepage: https://github.com/fs0414/rbcsv
|
|
68
77
|
licenses:
|
|
69
78
|
- MIT
|