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.
@@ -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(processed.as_bytes());
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 parse_csv_with_options(input: &str, options: &CsvParseOptions) -> Result<Vec<Vec<String>>, CsvError> {
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 parse_csv_file_with_options(file_path: &str, options: &CsvParseOptions) -> Result<Vec<Vec<String>>, CsvError> {
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
+ }
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RbCsv
4
- VERSION = "0.1.7"
4
+ VERSION = "0.2.0"
5
5
  end
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.1.7
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
- - benchmark.rb
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