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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +23 -0
- data/Cargo.lock +1 -1
- data/DEVELOPMENT.md +121 -11
- data/README.md +89 -67
- 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/{test_write_functionality.rb → examples/features/test_write_functionality.rb} +1 -1
- data/ext/rbcsv/Cargo.toml +1 -1
- data/ext/rbcsv/src/error.rs +2 -2
- data/ext/rbcsv/src/lib.rs +8 -1
- data/ext/rbcsv/src/parser.rs +74 -15
- data/ext/rbcsv/src/ruby_api.rs +101 -2
- data/ext/rbcsv/src/value.rs +87 -0
- data/lib/rbcsv/version.rb +1 -1
- metadata +16 -8
- 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
|
@@ -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 "=== テスト完了 ==="
|
data/ext/rbcsv/Cargo.toml
CHANGED
data/ext/rbcsv/src/error.rs
CHANGED
|
@@ -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
|
|
data/ext/rbcsv/src/parser.rs
CHANGED
|
@@ -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";
|
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, 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
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,23 +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
|
-
- test_write_functionality.rb
|
|
68
76
|
homepage: https://github.com/fs0414/rbcsv
|
|
69
77
|
licenses:
|
|
70
78
|
- MIT
|