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
|
@@ -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 "=== テスト完了 ==="
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# RbCsv.write() 機能のテストスクリプト
|
|
5
|
+
#
|
|
6
|
+
# 実行方法:
|
|
7
|
+
# ruby test_write_functionality.rb
|
|
8
|
+
#
|
|
9
|
+
# 前提条件:
|
|
10
|
+
# - bundle exec rake compile でライブラリがビルド済みであること
|
|
11
|
+
|
|
12
|
+
require_relative '../../lib/rbcsv'
|
|
13
|
+
require 'fileutils'
|
|
14
|
+
|
|
15
|
+
class RbCsvWriteTest
|
|
16
|
+
def initialize
|
|
17
|
+
@test_dir = '/tmp/rbcsv_write_tests'
|
|
18
|
+
@success_count = 0
|
|
19
|
+
@total_count = 0
|
|
20
|
+
setup_test_directory
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def run_all_tests
|
|
24
|
+
puts "=" * 60
|
|
25
|
+
puts "RbCsv.write() 機能テスト開始"
|
|
26
|
+
puts "=" * 60
|
|
27
|
+
puts
|
|
28
|
+
|
|
29
|
+
test_basic_write
|
|
30
|
+
test_roundtrip_write_read
|
|
31
|
+
test_file_overwrite
|
|
32
|
+
test_empty_data_error
|
|
33
|
+
test_field_count_mismatch_error
|
|
34
|
+
test_single_row_write
|
|
35
|
+
test_unicode_content
|
|
36
|
+
test_special_characters
|
|
37
|
+
|
|
38
|
+
puts
|
|
39
|
+
puts "=" * 60
|
|
40
|
+
puts "テスト結果: #{@success_count}/#{@total_count} 成功"
|
|
41
|
+
puts "=" * 60
|
|
42
|
+
|
|
43
|
+
cleanup_test_directory
|
|
44
|
+
|
|
45
|
+
if @success_count == @total_count
|
|
46
|
+
puts "✅ すべてのテストが成功しました!"
|
|
47
|
+
exit 0
|
|
48
|
+
else
|
|
49
|
+
puts "❌ 一部のテストが失敗しました"
|
|
50
|
+
exit 1
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def setup_test_directory
|
|
57
|
+
FileUtils.mkdir_p(@test_dir)
|
|
58
|
+
puts "テストディレクトリを作成: #{@test_dir}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def cleanup_test_directory
|
|
62
|
+
FileUtils.rm_rf(@test_dir)
|
|
63
|
+
puts "テストディレクトリを削除: #{@test_dir}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def run_test(name)
|
|
67
|
+
@total_count += 1
|
|
68
|
+
print "#{name}... "
|
|
69
|
+
|
|
70
|
+
begin
|
|
71
|
+
yield
|
|
72
|
+
@success_count += 1
|
|
73
|
+
puts "✅ 成功"
|
|
74
|
+
rescue => e
|
|
75
|
+
puts "❌ 失敗: #{e.message}"
|
|
76
|
+
puts " #{e.backtrace.first}"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def test_basic_write
|
|
81
|
+
run_test("基本的なCSV書き込み") do
|
|
82
|
+
file_path = File.join(@test_dir, 'basic.csv')
|
|
83
|
+
data = [
|
|
84
|
+
['name', 'age', 'city'],
|
|
85
|
+
['Alice', '25', 'Tokyo'],
|
|
86
|
+
['Bob', '30', 'Osaka']
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
RbCsv.write(file_path, data)
|
|
90
|
+
|
|
91
|
+
content = File.read(file_path)
|
|
92
|
+
expected = "name,age,city\nAlice,25,Tokyo\nBob,30,Osaka\n"
|
|
93
|
+
|
|
94
|
+
raise "書き込み内容が期待値と異なります" unless content == expected
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def test_roundtrip_write_read
|
|
99
|
+
run_test("書き込み→読み込みの往復テスト") do
|
|
100
|
+
file_path = File.join(@test_dir, 'roundtrip.csv')
|
|
101
|
+
original_data = [
|
|
102
|
+
['product', 'price', 'category'],
|
|
103
|
+
['Apple', '100', 'Fruit'],
|
|
104
|
+
['Carrot', '50', 'Vegetable']
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
RbCsv.write(file_path, original_data)
|
|
108
|
+
read_data = RbCsv.read(file_path)
|
|
109
|
+
|
|
110
|
+
raise "往復テストで元データと異なります" unless original_data == read_data
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def test_file_overwrite
|
|
115
|
+
run_test("ファイル上書きテスト") do
|
|
116
|
+
file_path = File.join(@test_dir, 'overwrite.csv')
|
|
117
|
+
|
|
118
|
+
# 最初のデータ
|
|
119
|
+
first_data = [['old'], ['data']]
|
|
120
|
+
RbCsv.write(file_path, first_data)
|
|
121
|
+
first_content = File.read(file_path)
|
|
122
|
+
|
|
123
|
+
# 上書き
|
|
124
|
+
second_data = [['new', 'header'], ['updated', 'content']]
|
|
125
|
+
RbCsv.write(file_path, second_data)
|
|
126
|
+
second_content = File.read(file_path)
|
|
127
|
+
|
|
128
|
+
expected_first = "old\ndata\n"
|
|
129
|
+
expected_second = "new,header\nupdated,content\n"
|
|
130
|
+
|
|
131
|
+
raise "最初の書き込み内容が不正" unless first_content == expected_first
|
|
132
|
+
raise "上書き後の内容が不正" unless second_content == expected_second
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def test_empty_data_error
|
|
137
|
+
run_test("空データエラーテスト") do
|
|
138
|
+
file_path = File.join(@test_dir, 'empty.csv')
|
|
139
|
+
|
|
140
|
+
error_raised = false
|
|
141
|
+
begin
|
|
142
|
+
RbCsv.write(file_path, [])
|
|
143
|
+
rescue RuntimeError => e
|
|
144
|
+
error_raised = true
|
|
145
|
+
raise "エラーメッセージが期待値と異なります" unless e.message.include?("CSV data is empty")
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
raise "空データでエラーが発生しませんでした" unless error_raised
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def test_field_count_mismatch_error
|
|
153
|
+
run_test("フィールド数不一致エラーテスト") do
|
|
154
|
+
file_path = File.join(@test_dir, 'mismatch.csv')
|
|
155
|
+
inconsistent_data = [
|
|
156
|
+
['name', 'age'],
|
|
157
|
+
['Alice', '25', 'Tokyo'] # 3フィールド(期待は2フィールド)
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
error_raised = false
|
|
161
|
+
begin
|
|
162
|
+
RbCsv.write(file_path, inconsistent_data)
|
|
163
|
+
rescue RuntimeError => e
|
|
164
|
+
error_raised = true
|
|
165
|
+
unless e.message.include?("Field count mismatch") && e.message.include?("line 2")
|
|
166
|
+
raise "エラーメッセージが期待値と異なります: #{e.message}"
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
raise "フィールド数不一致でエラーが発生しませんでした" unless error_raised
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def test_single_row_write
|
|
175
|
+
run_test("単一行書き込みテスト") do
|
|
176
|
+
file_path = File.join(@test_dir, 'single.csv')
|
|
177
|
+
data = [['single', 'row', 'test']]
|
|
178
|
+
|
|
179
|
+
RbCsv.write(file_path, data)
|
|
180
|
+
content = File.read(file_path)
|
|
181
|
+
expected = "single,row,test\n"
|
|
182
|
+
|
|
183
|
+
raise "単一行の書き込み内容が不正" unless content == expected
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def test_unicode_content
|
|
188
|
+
run_test("Unicode文字テスト") do
|
|
189
|
+
file_path = File.join(@test_dir, 'unicode.csv')
|
|
190
|
+
data = [
|
|
191
|
+
['名前', '年齢', '都市'],
|
|
192
|
+
['田中太郎', '30', '東京'],
|
|
193
|
+
['山田花子', '25', '大阪'],
|
|
194
|
+
['🎉', '😀', '🌸']
|
|
195
|
+
]
|
|
196
|
+
|
|
197
|
+
RbCsv.write(file_path, data)
|
|
198
|
+
read_data = RbCsv.read(file_path)
|
|
199
|
+
|
|
200
|
+
raise "Unicode文字の往復テストが失敗" unless data == read_data
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def test_special_characters
|
|
205
|
+
run_test("特殊文字テスト") do
|
|
206
|
+
file_path = File.join(@test_dir, 'special.csv')
|
|
207
|
+
data = [
|
|
208
|
+
['field1', 'field2', 'field3'],
|
|
209
|
+
['comma,test', 'quote"test', 'newline\ntest'],
|
|
210
|
+
['tab\ttest', 'backslash\\test', 'normal']
|
|
211
|
+
]
|
|
212
|
+
|
|
213
|
+
RbCsv.write(file_path, data)
|
|
214
|
+
read_data = RbCsv.read(file_path)
|
|
215
|
+
|
|
216
|
+
raise "特殊文字の往復テストが失敗" unless data == read_data
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# テスト実行
|
|
222
|
+
if __FILE__ == $0
|
|
223
|
+
tester = RbCsvWriteTest.new
|
|
224
|
+
tester.run_all_tests
|
|
225
|
+
end
|
data/ext/rbcsv/Cargo.toml
CHANGED
data/ext/rbcsv/src/error.rs
CHANGED
|
@@ -19,6 +19,10 @@ pub enum ErrorKind {
|
|
|
19
19
|
FieldCountMismatch,
|
|
20
20
|
// 空のCSVデータ
|
|
21
21
|
EmptyData,
|
|
22
|
+
// 書き込み権限エラー
|
|
23
|
+
WritePermission,
|
|
24
|
+
// 無効なデータエラー
|
|
25
|
+
InvalidData,
|
|
22
26
|
// その他のエラー
|
|
23
27
|
#[allow(dead_code)]
|
|
24
28
|
Other,
|
|
@@ -32,6 +36,8 @@ impl fmt::Display for CsvError {
|
|
|
32
36
|
ErrorKind::Encoding => write!(f, "Encoding Error: {}", self.message),
|
|
33
37
|
ErrorKind::FieldCountMismatch => write!(f, "Field Count Mismatch: {}", self.message),
|
|
34
38
|
ErrorKind::EmptyData => write!(f, "Empty Data: {}", self.message),
|
|
39
|
+
ErrorKind::WritePermission => write!(f, "Write Permission Error: {}", self.message),
|
|
40
|
+
ErrorKind::InvalidData => write!(f, "Invalid Data Error: {}", self.message),
|
|
35
41
|
ErrorKind::Other => write!(f, "Error: {}", self.message),
|
|
36
42
|
}
|
|
37
43
|
}
|
|
@@ -70,9 +76,17 @@ impl CsvError {
|
|
|
70
76
|
pub fn empty_data() -> Self {
|
|
71
77
|
Self::new(ErrorKind::EmptyData, "CSV data is empty")
|
|
72
78
|
}
|
|
79
|
+
|
|
80
|
+
pub fn write_permission(message: impl Into<String>) -> Self {
|
|
81
|
+
Self::new(ErrorKind::WritePermission, message)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
pub fn invalid_data(message: impl Into<String>) -> Self {
|
|
85
|
+
Self::new(ErrorKind::InvalidData, message)
|
|
86
|
+
}
|
|
73
87
|
}
|
|
74
88
|
|
|
75
|
-
// csv crate
|
|
89
|
+
// csv crate error to CsvError conversion
|
|
76
90
|
impl From<csv::Error> for CsvError {
|
|
77
91
|
fn from(err: csv::Error) -> Self {
|
|
78
92
|
match err.kind() {
|
|
@@ -84,4 +98,4 @@ impl From<csv::Error> for CsvError {
|
|
|
84
98
|
_ => CsvError::parse(err.to_string()),
|
|
85
99
|
}
|
|
86
100
|
}
|
|
87
|
-
}
|
|
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};
|
|
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> {
|
|
@@ -13,6 +14,13 @@ fn init(ruby: &Ruby) -> Result<(), magnus::Error> {
|
|
|
13
14
|
module.define_singleton_method("parse!", magnus::function!(parse_trim, 1))?;
|
|
14
15
|
module.define_singleton_method("read", magnus::function!(read, 1))?;
|
|
15
16
|
module.define_singleton_method("read!", magnus::function!(read_trim, 1))?;
|
|
17
|
+
module.define_singleton_method("write", magnus::function!(write, 2))?;
|
|
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))?;
|
|
16
24
|
|
|
17
25
|
Ok(())
|
|
18
26
|
}
|