rbcsv 0.1.8 → 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.
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../../lib/rbcsv'
5
+
6
+ puts "=== RbCsv 型認識機能テスト ==="
7
+ puts
8
+
9
+ # テストデータ
10
+ csv_data = <<~CSV
11
+ name,age,score,rating
12
+ Alice,25,85.5,A
13
+ Bob,30,92,B+
14
+ Charlie,0,100.0,S
15
+ CSV
16
+
17
+ puts "元のCSVデータ:"
18
+ puts csv_data
19
+ puts
20
+
21
+ # 通常のparseテスト(すべて文字列)
22
+ puts "1. RbCsv.parse (すべて文字列):"
23
+ result = RbCsv.parse(csv_data)
24
+ result.each_with_index do |row, i|
25
+ puts "Row #{i}: #{row.inspect}"
26
+ if i > 0 # ヘッダー以外
27
+ puts " age (#{row[1].class}): #{row[1]}"
28
+ puts " score (#{row[2].class}): #{row[2]}"
29
+ end
30
+ end
31
+ puts
32
+
33
+ # 型認識parseテスト
34
+ puts "2. RbCsv.parse_typed (数値は数値型):"
35
+ result_typed = RbCsv.parse_typed(csv_data)
36
+ result_typed.each_with_index do |row, i|
37
+ puts "Row #{i}: #{row.inspect}"
38
+ if i > 0 # ヘッダー以外
39
+ puts " age (#{row[1].class}): #{row[1]}"
40
+ puts " score (#{row[2].class}): #{row[2]}"
41
+ puts " 計算可能: age * 2 = #{row[1] * 2}"
42
+ end
43
+ end
44
+ puts
45
+
46
+ # エッジケースのテスト
47
+ edge_case_csv = <<~CSV
48
+ type,value
49
+ integer,123
50
+ negative,-456
51
+ float,45.6
52
+ scientific,1.23e-4
53
+ empty,
54
+ text,hello world
55
+ mixed,123abc
56
+ CSV
57
+
58
+ puts "3. エッジケーステスト:"
59
+ puts "CSVデータ:"
60
+ puts edge_case_csv
61
+ puts
62
+
63
+ puts "RbCsv.parse_typed の結果:"
64
+ result_edge = RbCsv.parse_typed(edge_case_csv)
65
+ result_edge.each_with_index do |row, i|
66
+ if i > 0 # ヘッダー以外
67
+ value = row[1]
68
+ type_name = value.class.name
69
+ puts "#{row[0]}: #{value.inspect} (#{type_name})"
70
+ end
71
+ end
72
+ puts
73
+
74
+ # trim版のテスト
75
+ csv_with_spaces = " name , age , score \n Alice , 25 , 85.5 "
76
+
77
+ puts "4. RbCsv.parse_typed! (trim + 型認識):"
78
+ puts "CSVデータ(空白付き): #{csv_with_spaces.inspect}"
79
+ result_trim = RbCsv.parse_typed!(csv_with_spaces)
80
+ result_trim.each do |row|
81
+ puts "Row: #{row.inspect}"
82
+ end
83
+ puts
84
+
85
+ # ファイル書き込み→型認識読み込みテスト
86
+ test_file = '/tmp/test_typed.csv'
87
+ write_data = [
88
+ ['product', 'price', 'quantity', 'in_stock'],
89
+ ['Apple', '100', '50', 'true'],
90
+ ['Orange', '80.5', '30', 'false'],
91
+ ['Banana', '60.25', '0', 'yes']
92
+ ]
93
+
94
+ puts "5. ファイル書き込み→型認識読み込みテスト:"
95
+ RbCsv.write(test_file, write_data)
96
+ puts "書き込み完了: #{test_file}"
97
+
98
+ read_typed = RbCsv.read_typed(test_file)
99
+ puts "RbCsv.read_typed の結果:"
100
+ read_typed.each_with_index do |row, i|
101
+ puts "Row #{i}: #{row.inspect}"
102
+ if i > 0
103
+ puts " price (#{row[1].class}): #{row[1]}"
104
+ puts " quantity (#{row[2].class}): #{row[2]}"
105
+ end
106
+ end
107
+ puts
108
+
109
+ puts "=== テスト完了 ==="
@@ -9,7 +9,7 @@
9
9
  # 前提条件:
10
10
  # - bundle exec rake compile でライブラリがビルド済みであること
11
11
 
12
- require_relative 'lib/rbcsv'
12
+ require_relative '../../lib/rbcsv'
13
13
  require 'fileutils'
14
14
 
15
15
  class RbCsvWriteTest
data/ext/rbcsv/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "rbcsv"
3
- version = "0.1.0"
3
+ version = "0.2.0"
4
4
  edition = "2021"
5
5
  authors = ["fujitani sora <fujitanisora0414@gmail.com>"]
6
6
  license = "MIT"
@@ -86,7 +86,7 @@ impl CsvError {
86
86
  }
87
87
  }
88
88
 
89
- // csv crateのエラーからの変換
89
+ // csv crate error to CsvError conversion
90
90
  impl From<csv::Error> for CsvError {
91
91
  fn from(err: csv::Error) -> Self {
92
92
  match err.kind() {
@@ -98,4 +98,4 @@ impl From<csv::Error> for CsvError {
98
98
  _ => CsvError::parse(err.to_string()),
99
99
  }
100
100
  }
101
- }
101
+ }
data/ext/rbcsv/src/lib.rs CHANGED
@@ -1,9 +1,10 @@
1
1
  mod error;
2
2
  mod parser;
3
3
  mod ruby_api;
4
+ mod value;
4
5
 
5
6
  use magnus::{Object, Ruby};
6
- use ruby_api::{parse, parse_trim, read, read_trim, write};
7
+ use ruby_api::{parse, parse_trim, read, read_trim, write, parse_typed, parse_typed_trim, read_typed, read_typed_trim};
7
8
 
8
9
  #[magnus::init]
9
10
  fn init(ruby: &Ruby) -> Result<(), magnus::Error> {
@@ -15,6 +16,12 @@ fn init(ruby: &Ruby) -> Result<(), magnus::Error> {
15
16
  module.define_singleton_method("read!", magnus::function!(read_trim, 1))?;
16
17
  module.define_singleton_method("write", magnus::function!(write, 2))?;
17
18
 
19
+ // typed variants
20
+ module.define_singleton_method("parse_typed", magnus::function!(parse_typed, 1))?;
21
+ module.define_singleton_method("parse_typed!", magnus::function!(parse_typed_trim, 1))?;
22
+ module.define_singleton_method("read_typed", magnus::function!(read_typed, 1))?;
23
+ module.define_singleton_method("read_typed!", magnus::function!(read_typed_trim, 1))?;
24
+
18
25
  Ok(())
19
26
  }
20
27
 
@@ -2,7 +2,6 @@ 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)]
7
6
  #[allow(dead_code)]
8
7
  pub struct CsvParseOptions {
@@ -17,8 +16,7 @@ impl Default for CsvParseOptions {
17
16
  }
18
17
  }
19
18
 
20
- /// エスケープシーケンスを実際の文字に変換
21
- pub fn escape_sanitize(s: &str) -> String {
19
+ pub fn _escape_sanitize(s: &str) -> String {
22
20
  s.replace("\\n", "\n")
23
21
  .replace("\\r", "\r")
24
22
  .replace("\\t", "\t")
@@ -28,14 +26,13 @@ pub fn escape_sanitize(s: &str) -> String {
28
26
 
29
27
  /// 基本的なCSVパース処理
30
28
  pub fn parse_csv_core(input: &str, trim_config: csv::Trim) -> Result<Vec<Vec<String>>, CsvError> {
31
- // 空のデータチェック
29
+ println!("input: {:?}", input);
32
30
  if input.trim().is_empty() {
33
31
  return Err(CsvError::empty_data());
34
32
  }
35
33
 
36
- // CSV crate に任せて適切なパースを行う(escape_sanitize は削除)
37
34
  let mut reader = csv::ReaderBuilder::new()
38
- .has_headers(false) // ヘッダーを無効にして、すべての行を読み込む
35
+ .has_headers(false)
39
36
  .trim(trim_config)
40
37
  .from_reader(input.as_bytes());
41
38
 
@@ -48,7 +45,6 @@ pub fn parse_csv_core(input: &str, trim_config: csv::Trim) -> Result<Vec<Vec<Str
48
45
  records.push(row);
49
46
  }
50
47
  Err(e) => {
51
- // フィールド数不一致エラーを詳細化
52
48
  if let csv::ErrorKind::UnequalLengths { expected_len, len, .. } = e.kind() {
53
49
  let error_msg = format!(
54
50
  "Field count mismatch at line {}: expected {} fields, got {} fields",
@@ -59,7 +55,6 @@ pub fn parse_csv_core(input: &str, trim_config: csv::Trim) -> Result<Vec<Vec<Str
59
55
  return Err(CsvError::new(ErrorKind::FieldCountMismatch, error_msg));
60
56
  }
61
57
 
62
- // その他のcsvエラーを自動変換
63
58
  return Err(CsvError::from(e));
64
59
  }
65
60
  }
@@ -108,17 +103,81 @@ pub fn parse_csv_file(file_path: &str, trim_config: csv::Trim) -> Result<Vec<Vec
108
103
  parse_csv_core(&content, trim_config)
109
104
  }
110
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
+
111
177
  #[cfg(test)]
112
178
  mod tests {
113
179
  use super::*;
114
180
 
115
- #[test]
116
- fn test_escape_sanitize() {
117
- let input = "Hello\\nWorld\\t\\\"Test\\\"\\\\End";
118
- let expected = "Hello\nWorld\t\"Test\"\\End";
119
- assert_eq!(escape_sanitize(input), expected);
120
- }
121
-
122
181
  #[test]
123
182
  fn test_parse_csv_core_basic() {
124
183
  let csv_data = "a,b,c\n1,2,3";
@@ -1,5 +1,5 @@
1
- use magnus::{Error as MagnusError, Ruby};
2
- use crate::parser::{parse_csv_core, parse_csv_file, write_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
  ///
@@ -67,6 +67,105 @@ pub fn write(ruby: &Ruby, file_path: String, data: Vec<Vec<String>>) -> Result<(
67
67
  .map_err(|e| MagnusError::new(ruby.exception_runtime_error(), e.to_string()))
68
68
  }
69
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
+ }
70
169
 
71
170
  #[cfg(test)]
72
171
  mod tests {
@@ -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.8"
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.8
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - fujitani sora
@@ -48,23 +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
- - test_write_functionality.rb
68
76
  homepage: https://github.com/fs0414/rbcsv
69
77
  licenses:
70
78
  - MIT