uniprop 0.1.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,403 @@
1
+ module UniProp
2
+ class PropData
3
+ # メタデータを生成する
4
+ # @param [Pathname] output_path メタデータを生成するパス
5
+ # @param [Version] generated_version 作成するメタデータのバージョン
6
+ # @param [Version/EfficientVersion] using_version generated_versionのメタデータの生成に使用されるバージョン
7
+ # @raise [FileExistsError] pathに既にファイルが存在している場合に発生。生成されたメタデータを修正した後に再度メソッドを実行してしまい、メタデータが上書きされる事を防ぐための措置。
8
+ # @raise [MetaDataExistsError] generated_versionのメタデータが既に存在する場合に発生
9
+ def generate_metadata(output_path, using_version, generated_version)
10
+ if output_path.exist?
11
+ raise FileExistsError.new(output_path)
12
+ end
13
+
14
+ generated_metadata = {}
15
+ generated_metadata["version_names"] = metadata.version_names
16
+ generated_metadata["version_metadatas"] = metadata.raw_version_metadatas
17
+ generated_metadata["version_metadatas"] << generate_version_metadata(using_version, generated_version)
18
+
19
+ generated_metadata["version_metadatas"].sort_by! { Version.name_to_weight(_1["version_name"]) }
20
+
21
+ output_path.write(JSON.pretty_generate(generated_metadata))
22
+ end
23
+
24
+ # @return [Hash<String,Object>] version_metadataに相当するHash
25
+ def generate_version_metadata(using_version, generated_version)
26
+ if using_version.prop_data != generated_version.prop_data
27
+ raise PropDataDifferentError, "Unable to recreate metadata because the PropData objects of two versions passed when initialized are different."
28
+ end
29
+
30
+ prop_data = using_version.prop_data
31
+
32
+ if prop_data.metadata.has_raw_version_metadata?(generated_version.version_name)
33
+ raise MetaDataExistsError.new(generated_version.version_name)
34
+ end
35
+
36
+ recreator = VersionMetaDataRecreator.new(using_version, generated_version)
37
+
38
+ version_metadata = {}
39
+ version_metadata["version_name"] = generated_version.version_name
40
+ version_metadata["file_formats"] = recreator.generate_file_formats
41
+
42
+ if generated_version.has_unihan?
43
+ version_metadata["unihan_files"] = generated_version.unihanprop.unihan_files.map { _1.basename_prefix }
44
+ version_metadata["unihan_properties"] = generated_version.unihanprop.unihan_properties.map { _1.longest_alias }
45
+ end
46
+
47
+ version_metadata
48
+ end
49
+
50
+ # PropDataオブジェクト作成時に渡したメタデータを使用し、プロパティ中心のメタデータを生成
51
+ # @param [EfficientVersion] version 生成するバージョン
52
+ # @param [Pathname] output_path メタデータを生成するパス
53
+ def generate_property_metadata(output_path, version)
54
+ if property_metadata.has_raw_version_metadata?(version.version_name)
55
+ return
56
+ end
57
+
58
+ md = property_metadata.raw_version_metadatas
59
+
60
+ if metadata.has_raw_version_metadata?(version.version_name)
61
+ puts "generating property metadata for #{version.version_name} ... "
62
+ md << generate_vsn_property_metadata(version)
63
+ else
64
+ raise MetaDataNotFoundError, "metadata for #{version.version_name} is not found."
65
+ end
66
+
67
+ output_path.write(JSON.pretty_generate(md))
68
+ end
69
+
70
+ # バージョン内のすべてのプロパティに対し、プロパティ中心のメタデータを生成
71
+ # @param [EfficientVersion] version
72
+ # @return [Hash<String,Object>] versionのプロパティ中心のメタデータ
73
+ def generate_vsn_property_metadata(version)
74
+ version_property_metadata = {}
75
+ version_property_metadata["version_name"] = version.version_name
76
+ version_property_metadata["properties"] = version.properties.map { generate_prop_property_metadata(version, _1) }
77
+ version_property_metadata
78
+ end
79
+
80
+ # プロパティに関するメタデータを生成
81
+ # @param [EfficientVersion] version
82
+ # @param [Property] property version内のプロパティ
83
+ # @return [Hash<String,Object>] プロパティに関するメタデータ
84
+ def generate_prop_property_metadata(version, property)
85
+ property_metadata = {}
86
+ property_metadata["property_name"] = property.longest_alias
87
+
88
+ property_metadata["positions"] = []
89
+ positions = property.actual_positions.to_a
90
+
91
+ positions.each do |position|
92
+ property_metadata["positions"] << {
93
+ "file_name" => position.propfile.basename_prefix,
94
+ "block" => position.block,
95
+ "range" => position.range.to_s,
96
+ "columns" => position.columns.size==1 ? position.columns[0] : position.columns
97
+ }
98
+ end
99
+
100
+ property_metadata["unihan"] = property.is_unihan_property?
101
+ property_metadata["type"] = property.property_value_type.to_s
102
+ property_metadata["derived"] = positions.any? { _1.propfile.is_derived? }
103
+
104
+ property_metadata
105
+ end
106
+ end
107
+
108
+ class VersionMetaDataRecreator
109
+ attr_reader :using_version, :using_version_metadata, :generated_version
110
+ # using_versionのメタデータを使用してgenerated_versionのメタデータを作成するためのオブジェクトを生成
111
+ # @param [Version] using_version
112
+ # @param [Version/EfficientVersion] generated_version
113
+ def initialize(using_version, generated_version)
114
+ @using_version = using_version
115
+ @using_version_metadata = using_version.version_metadata
116
+ @generated_version = generated_version
117
+ end
118
+
119
+ # メタデータのfile_formats項を生成
120
+ # @return [Array<Hash<String,Object>>]
121
+ def generate_file_formats
122
+ return @file_formats if @file_formats
123
+ @file_formats = []
124
+
125
+ using_version.files.each_with_index do |file, i|
126
+ puts "recreating metadata for #{file.basename_prefix} (#{i+1}/#{using_version.files.size})"
127
+
128
+ next if !using_version_metadata.has_propfile_metadata?(file)
129
+ next if !generated_version.has_file?(file.basename_prefix)
130
+
131
+ @file_formats << { "file_name"=>file.basename_prefix, "blocks"=>generate_blocks(file) }
132
+ end
133
+
134
+ @file_formats
135
+ end
136
+
137
+ # メタデータのblocks項を生成
138
+ # @param [PropFile] using_file
139
+ def generate_blocks(using_file)
140
+ using_filename = using_file.basename_prefix
141
+ generated_file = generated_version.find_file(using_filename)
142
+ using_file_metadata = using_version_metadata.find_propfile_metadata(using_file)
143
+
144
+ block_generator = BlockGenerator.new(using_file, generated_file, using_file_metadata)
145
+
146
+ result_blocks = []
147
+
148
+ block_generator.generate_raw_blocks.each do |raw_block|
149
+ result_block = {}
150
+ result_block["content"] = raw_block.content
151
+ result_block["range"] = raw_block.range
152
+ result_blocks << result_block
153
+ end
154
+
155
+ result_blocks
156
+ end
157
+ end
158
+
159
+ # BlockGenerator で使用するStruct
160
+ # @param [Symbol] type propertyのproperty_value_type
161
+ # @param [Property] property
162
+ Format = Struct.new(:type, :property)
163
+
164
+ # 既存のメタデータを使用し、メタデータ未知のPropFileに関するblocksを作成するクラス
165
+ class BlockGenerator
166
+ attr_reader :using_file, :generated_file, :using_file_metadata, :using_version_metadata, :generated_version
167
+
168
+ # @param [PropFile] using_file
169
+ # @param [PropFile] generated_file
170
+ # @param [PropFileMetaData] using_file_metadata using_fileのPropFileMetaData
171
+ def initialize(using_file, generated_file, using_file_metadata)
172
+ @using_file = using_file
173
+ @generated_file = generated_file
174
+ @generated_version = generated_file.version
175
+ @using_file_metadata = using_file_metadata
176
+ @using_version_metadata = using_file.version.version_metadata
177
+ end
178
+
179
+ # generated_fileのblocksに相当するArray<RawBlock>を作成
180
+ # @return [Array<RawBlock>]
181
+ def generate_raw_blocks
182
+ return @raw_blocks if @raw_blocks
183
+ @raw_blocks = []
184
+
185
+ using_file_metadata.blocks.size.times do |block_no|
186
+ range = matched_ranges[block_no]
187
+
188
+ if range
189
+ content = using_file_metadata.raw_blocks[block_no].content
190
+ @raw_blocks << RawBlock.new(content, range.to_s)
191
+ end
192
+ end
193
+
194
+ @raw_blocks
195
+ end
196
+
197
+ # using_fileのblocksのフォーマットを取得
198
+ # @return [Array<Array<Object>>]
199
+ # @note 返り値のn番目の要素はblocks内のn番目のblockのフォーマットに対応
200
+ def block_format_types
201
+ @block_format_types ||= using_file_metadata.blocks.map { block_format_type(_1.content) }
202
+ end
203
+
204
+ # using_fileに、binary,enumerated,catalog以外の型のプロパティを含むブロックが2つ以上存在するか判定
205
+ # @return [Boolean] 存在する場合true
206
+ def has_multiple_free_value_block?
207
+ return @multiple_free_value_block_f if @multiple_free_value_block_f
208
+
209
+ free_value_exist_f = []
210
+ using_file_metadata.blocks.each do |block|
211
+ block_properties = block.content.reject { _1.class == Array }
212
+ .compact
213
+
214
+ free_value_exist_f << !block_properties.all? { _1.property_value_type==:binary ||
215
+ _1.property_value_type==:enumerated ||
216
+ _1.property_value_type==:catalog }
217
+ end
218
+
219
+ @multiple_free_value_block_f = free_value_exist_f.filter { _1 }.size > 1
220
+ @multiple_free_value_block_f
221
+ end
222
+
223
+ # blockの再生成に使用するフォーマットとして最も適切なものを取得
224
+ # @param [Array<Property?>/Array<Array<Property?>>]] Block.contentの値
225
+ # @return [Array<Object>]
226
+ def block_format_type(content)
227
+ content.map { column_format_type(_1) }
228
+ end
229
+
230
+ def column_format_type(column)
231
+ # columnがArrayの場合、その列には固有の表記法が使用されており、一様に判定を行う事はできないため、:text を返す
232
+ return Format.new(:text, column) if column.class == Array
233
+
234
+ # columnがnilの場合、nil (その列には判定を行わない)
235
+ return nil if !column
236
+
237
+ if has_multiple_free_value_block?
238
+ # 列挙型以外のプロパティを取るブロックが2つ以上存在する場合、
239
+ # コードポイントと値の対応を調べないと、同じプロパティに関する記述かの判定が不可能
240
+ return column
241
+ else
242
+ # 列挙型以外のプロパティを取るブロックが1つだけの場合、
243
+ # 記述されている値の型を調べるだけで、同じプロパティに関する記述かの判定が可能
244
+ type = (column.property_value_type==:miscellaneous) ? column.miscellaneous_format : column.property_value_type
245
+
246
+ # type==:uniqueの場合、判定時には:textと同様に扱いたいため、:textに変更
247
+ type = :text if type==:unique
248
+
249
+ return Format.new(type, column)
250
+ end
251
+ end
252
+
253
+ # generated_fileの各行が、何番目のブロックのblock_format_typeにマッチするか判定
254
+ # @return [Array<Integer?>] m行目がblocks内のn番目のblockのフォーマットに一致する場合、m番目の要素はn。どのブロックにもマッチしない場合、nil。
255
+ def lines_format
256
+ return @lines_format if @lines_format
257
+ @lines_format = []
258
+
259
+ generated_file.lines.size.times do |row|
260
+ # n行目がコメントの場合、line_formatによる判定は行わずnilを追加
261
+ if generated_file.comment?(row)
262
+ @lines_format << nil
263
+ next
264
+ end
265
+ @lines_format << line_format(row)
266
+ end
267
+
268
+ @lines_format
269
+ end
270
+
271
+ # @param [Integer] row
272
+ # @return [Array<Integer>] n番目のblockのフォーマットに一致する場合、n。どのブロックにもマッチしない場合、nil。
273
+ def line_format(row)
274
+ matched_blocks = []
275
+ using_file_metadata.blocks.size.times do |block_no|
276
+ codepoint_col_no = using_file_metadata.codepoint_column_nos[block_no]
277
+
278
+ matched_blocks << block_no if match_format?(row, codepoint_col_no, block_no)
279
+ end
280
+
281
+ if matched_blocks.size == 1
282
+ # line_formatの結果のサイズが1の場合
283
+ # row行目がマッチするブロックが一意に絞れているため、結果として使用
284
+ return matched_blocks[0]
285
+ else
286
+ # row行目がマッチするブロックが一意に絞れていない場合
287
+ # この場合、ある行がどのプロパティについて記述されているか、データファイルに記述されている場合が多い
288
+ # そのため、row行目の中に含まれるプロパティ名で判定
289
+
290
+ prop_including_blocks = []
291
+ using_file_metadata.blocks.size.times do |block_no|
292
+ using_file_metadata.blocks[block_no].content.compact.each do |prop|
293
+ next if prop.class == Array
294
+ prop_including_blocks << block_no if generated_file.has_property_alias?(row, prop)
295
+ end
296
+ end
297
+
298
+ if prop_including_blocks.size == 1
299
+ return prop_including_blocks[0]
300
+ else
301
+ # プロパティ名を使用しても一意に絞れない場合、nil
302
+ return nil
303
+ end
304
+ end
305
+ end
306
+
307
+ # row行目がformat_typeの形式に一致するか判定
308
+ # @param [Integer] row
309
+ # @param [Integer] codepoint_column_no
310
+ # @param [Integer] block_no
311
+ def match_format?(row, codepoint_column_no, block_no)
312
+ shaped_line = generated_file.shaped_lines[row]
313
+ format_type = block_format_types[block_no]
314
+
315
+ # row行目の中にcol_formatと異なるフォーマットの列が存在する場合、その時点でfalseを返す
316
+ format_type.each_with_index do |col_format, col_no|
317
+ next if !col_format # formatがnilの場合には判定を行わない
318
+
319
+ if col_format.class == Format
320
+ if [:enumerated, :catalog, :binary].include?(col_format.type) && generated_version.has_property?(col_format.property)
321
+ # 列挙型のプロパティ(enumerated, catalog, binary)の場合、 prop.version == generated_version でないと、propに新しいバージョンで追加された値が含まれず、プロパティの範囲の正確な推測が不可能
322
+ prop = generated_version.find_property(col_format.property)
323
+ else
324
+ prop = col_format.property
325
+ end
326
+
327
+ if generated_file.type_match?(row, col_no, col_format.type, prop)
328
+ next
329
+ else
330
+ return false
331
+ end
332
+
333
+ else
334
+ # col_format.class==Property の場合、型での判定はできない
335
+ # generated_fileのrow行目のcodepointがusing_fileで取っている値が、generated_fileと同じであるかを判定する
336
+ bvg = using_version_metadata.find_propfile_metadata(using_file).block_value_group(block_no)
337
+ shaped_line_dup = shaped_line.dup
338
+ codepoint = shaped_line_dup.delete_at(codepoint_column_no)
339
+
340
+ # codepointがnnnn..mmmm形式の場合、最初のnnnnのみを比較に使用
341
+ # nnnn..mmmmの範囲では同じ値を持つため、nnnnのみを使用すれば十分
342
+ codepoint = codepoint.split("..")[0]
343
+ generated_file_value = generated_file.value_at(row, col_no)
344
+ using_file_value = bvg.values_of(codepoint)
345
+
346
+ if using_file_value.class==String && using_file_value==generated_file_value ||
347
+ using_file_value.class==Array && using_file_value.include?(generated_file_value)
348
+ next
349
+ else
350
+ return false
351
+ end
352
+
353
+ end
354
+ end
355
+ true
356
+ end
357
+
358
+ # 各ブロックがgenerated_fileにおいてマッチした行数の範囲を取得
359
+ # @return [Array<Range<Integer>>]
360
+ # @note n番目の要素はn番目のブロックがマッチした範囲
361
+ def matched_ranges
362
+ return @matched_ranges if @matched_ranges
363
+
364
+ block_no_to_ranges = Hash.new { |hash,key| hash[key]=[] }
365
+
366
+ measured_block_no = nil # 範囲を計測中のblock_no
367
+ start_idx = nil # measured_block_noが最初に現れたidx
368
+ end_idx = nil # measured_block_noが最後に現れたidx
369
+ pre_idx = nil # 前回のblock_no(nil以外のInteger)が現れたidx
370
+
371
+ lines_format_dup = lines_format.dup
372
+ lines_format_dup << Float::NAN # 最後の要素まで処理を行うため、最後にNANを入れておく
373
+
374
+ lines_format_dup.each_with_index do |block_no, i|
375
+ next if !block_no # i行目がコメント行などの場合
376
+
377
+ if block_no != measured_block_no
378
+ # 前回までのmeasured_block_noの計測を終了
379
+ end_idx = pre_idx
380
+ if measured_block_no && start_idx && end_idx
381
+ block_no_to_ranges[measured_block_no] << Range.new(start_idx, end_idx)
382
+ end
383
+
384
+ # measured_block_noをblock_noに切り替え
385
+ measured_block_no = block_no
386
+ start_idx = i
387
+ end
388
+
389
+ pre_idx = i
390
+ end
391
+
392
+ # 同一ブロックが2つ以上に分かれた範囲に記述されていると判定されている場合、
393
+ # 最初の範囲だけをn番目のブロックの範囲として採用
394
+ # その場合、誤ったメタデータが生成されることになるが、検証メソッドにより検出され、ユーザに修正される
395
+ @matched_ranges = []
396
+ using_file_metadata.blocks.size.times do |block_no|
397
+ @matched_ranges[block_no] = block_no_to_ranges[block_no][0]
398
+ end
399
+
400
+ @matched_ranges
401
+ end
402
+ end
403
+ end