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,998 @@
1
+ module UniProp
2
+ module Alias
3
+ attr_reader :longest_alias
4
+
5
+ # 文字列を正規化
6
+ # @param [String] str 正規化前の文字列
7
+ # @return [String] strを正規化したもの
8
+ def self.canonical(str)
9
+ str.gsub(/[-_ ]/, '').downcase
10
+ end
11
+
12
+ # @note aliasはインスタンス化時にも追加可能だが、add_aliasを使用する事でも追加可能
13
+ # @param [*String] new_aliases 追加するalias(個数任意)
14
+ def initialize(*new_aliases)
15
+ new_aliases.each { add_alias _1 }
16
+ end
17
+
18
+ # aliasを追加
19
+ # @param [String] new_alias 追加するalias
20
+ # @note aliasはcanonicalを使用して正規化されて追加される
21
+ def add_alias(new_alias)
22
+ if new_alias.class == String
23
+ if new_alias.size > @longest_alias.to_s.size
24
+ @longest_alias = new_alias
25
+ end
26
+ aliases << Alias.canonical(new_alias)
27
+ uncanonicaled_aliases << new_alias
28
+ end
29
+ end
30
+
31
+ # :nocov:
32
+ # @return [Array<String>] 追加済みの正規化済みのalias
33
+ def aliases
34
+ @aliases ||= []
35
+ end
36
+
37
+ # @return [Array<String>] 追加済みのalias
38
+ def uncanonicaled_aliases
39
+ @uncanonicaled_aliases ||= []
40
+ end
41
+ # :nocov:
42
+
43
+ def has_alias?(alias_str)
44
+ return aliases.include?(Alias.canonical(alias_str))
45
+ end
46
+
47
+ # @return [Boolean] self.aliasesとother.aliasesが完全に同じ場合にtrue
48
+ def ==(other)
49
+ aliases==other.aliases
50
+ end
51
+
52
+ # @private
53
+ def eql?(other); self==other end
54
+
55
+ # @private
56
+ def hash
57
+ aliases.sort.join.hash
58
+ end
59
+ end
60
+
61
+ class Property
62
+ attr_reader :property_value_type, :version
63
+
64
+ include Alias
65
+
66
+ def initialize(version, *new_aliases)
67
+ @version = version
68
+ super(*new_aliases)
69
+ end
70
+
71
+ def property_value_type=(type)
72
+ type = type.downcase
73
+ if (
74
+ type == "catalog" ||
75
+ type == "enumerated" ||
76
+ type == "binary" ||
77
+ type == "string" ||
78
+ type == "numeric" ||
79
+ type == "miscellaneous"
80
+ )
81
+ @property_value_type = type.to_sym
82
+ else
83
+ raise PropertyValueTypeNotExistsError.new(type)
84
+ end
85
+ end
86
+
87
+ # settings.rbのmiscellaneous_formats内のformat_typeを小文字のシンボルで取得。記述されていない場合はnil
88
+ # @return [Symbol?]
89
+ def miscellaneous_format
90
+ version.property_to_miscellaneous_formats.dig(self, :format_type)&.downcase&.to_sym
91
+ end
92
+
93
+ # settings.rbのmiscellaneous_formats内のunique_thresholdを取得。記述されていない場合はnil
94
+ # @return [Integer?/Float?]
95
+ def unique_threshold
96
+ version.property_to_miscellaneous_formats.dig(self, :unique_threshold)
97
+ end
98
+
99
+ def ==(other)
100
+ # プロパティのエイリアスはバージョン更新時に増えることがあるため、versionの異なるPropertyを比較する場合には、プロパティが増えていてもtrueを返す
101
+ if version > other.version
102
+ return (other.aliases-aliases).empty?
103
+ elsif version < other.version
104
+ return (aliases-other.aliases).empty?
105
+ else
106
+ aliases==other.aliases
107
+ end
108
+ end
109
+
110
+ # @return [Array<PropertyValue>]
111
+ def property_values
112
+ version.property_to_property_values[self]
113
+ end
114
+
115
+ # :nocov:
116
+ # property_value_aliasをエイリアスに持つPropertyValueをproperty_valuesに持つか判定
117
+ # @param [String] property_value_alias
118
+ def has_property_value?(property_value_alias)
119
+ !!find_property_value(property_value_alias)
120
+ rescue
121
+ false
122
+ end
123
+ # :nocov:
124
+
125
+ # property_value_aliasをエイリアスに持つPropertyValueをproperty_valuesの中から取得
126
+ # @param [String] property_value_alias
127
+ # @return [PropertyValue]
128
+ # @raise [PropertyValueNotFoundError] 該当するPropertyValueが存在しない場合に発生
129
+ def find_property_value(property_value_alias)
130
+ pv = property_values.find { _1.has_alias?(property_value_alias)}
131
+
132
+ if pv
133
+ return pv
134
+ else
135
+ raise(PropertyValueNotFoundError, "#{longest_alias} doesn't have #{property_value_alias} as value.")
136
+ end
137
+ end
138
+
139
+ # :nocov:
140
+ # versionに含まれるPropFileの中の、このプロパティが含まれる場所を取得
141
+ # @return [Array<Position>]
142
+ def actual_positions
143
+ @actual_positions ||= version.version_metadata.property_to_actual_positions[self]
144
+ end
145
+ # :nocov:
146
+
147
+ # propfile中でプロパティが含まれる列を取得
148
+ # @note プロパティがpropfileに含まれない場合、空の配列を返す
149
+ # @param [PropFile] propfile
150
+ # @return [Array<Integer>]
151
+ def actual_columns(propfile)
152
+ columns = []
153
+ actual_positions.each { columns<<_1.column if _1.propfile==propfile }
154
+ columns
155
+ end
156
+
157
+ # Unihanのプロパティか判定
158
+ def is_unihan_property?
159
+ version.unihan_properties.include?(self)
160
+ end
161
+ end
162
+
163
+ class PropertyValue
164
+ include Alias
165
+
166
+ attr_accessor :property
167
+ def initialize(property, *new_aliases)
168
+ @property = property
169
+ new_aliases.each { add_alias _1 }
170
+ end
171
+ end
172
+
173
+ class PropFile
174
+ attr_accessor :version, :strip_regexp, :split_regexp, :basename_prefix
175
+
176
+ # @param [Pathname/String] path キャッシュのPathnameまたはbasename_prefixに相当するString
177
+ # @note fileをPathnameで指定する場合、絶対パスでの指定が必要
178
+ # @param [Regexp] strip_regexp 対応するファイル内で使われる空白文字の正規表現
179
+ # @param [Regexp] split_regexp 対応するファイル内で使われる区切り文字の正規表現
180
+ def initialize(path, version, strip_regexp: /\s+/, split_regexp: /;/)
181
+ if path.class==Pathname
182
+ @cache_path = path
183
+ @basename_prefix = UniPropUtils::FileManager.prefix(@cache_path)
184
+ else
185
+ @basename_prefix = path
186
+ end
187
+
188
+ # # strip_regexp, split_regexpが引数で指定されていない場合、settings.rbの記述を使用
189
+ # file_format = version.prop_data.find_settings_value(version.prop_data.unihan_files_information, "file_format", version.version_name)
190
+
191
+ # strip_regexp ||= file_format[:strip]
192
+ # split_regexp ||= file_format[:split]
193
+
194
+ @version = version
195
+ @strip_regexp = strip_regexp
196
+ @split_regexp = split_regexp
197
+ end
198
+
199
+ # @return [Pathname]
200
+ # @raise [FileNotFoundError] キャッシュが存在せず、ダウンロードにも失敗した場合に発生
201
+ def cache_path
202
+ return @cache_path if @cache_path
203
+
204
+ # キャッシュの中にbasename_prefixと同名のファイルがある場合、それを使用
205
+ if version.has_cache_file?(basename_prefix)
206
+ @cache_path = version.find_cache_file_path(basename_prefix)
207
+ return @cache_path
208
+ end
209
+
210
+ # キャッシュが保存されていない場合、ダウンロードを試みる
211
+ download_myself
212
+ if version.has_cache_file?(basename_prefix)
213
+ @cache_path = version.find_cache_file_path(basename_prefix)
214
+ return @cache_path
215
+ else
216
+ raise FileNotFoundError, "#{basename_prefix} does not exist in cache and download failed."
217
+ end
218
+ end
219
+
220
+ # :nocov:
221
+ # versionの、basename_prefixに該当するファイル名のファイルをUnicode.orgからダウンロード
222
+ def download_myself
223
+ version.download_file(basename_prefix)
224
+ end
225
+ # :nocov:
226
+
227
+ def is_meta_file?() false end
228
+
229
+ def is_unihan_file?() false end
230
+
231
+ # ファイルコンテンツを改行で区切った配列を取得
232
+ # @return [Array]
233
+ def lines() @lines ||= cache_path.readlines.map(&:chomp) end
234
+
235
+ # ファイルコンテンツからコメントを削除したものを改行で区切った配列を取得
236
+ # @return [Array]
237
+ # @note コメントのみからなる行は空文字列に変換されるだけであり、要素は削除されない (行数がインデックスと対応)
238
+ def lines_without_comment
239
+ @lines_without_comment ||= lines.map { |l| l.gsub(/#.*/,'') }
240
+ end
241
+
242
+ # lines_without_commentのうち、空文字列となった要素を削除した配列を取得
243
+ # @return [Array]
244
+ def netto_lines
245
+ @netto_lines ||= lines_without_comment.reject { |l| l.match(/^\s*$/) }
246
+ end
247
+
248
+ # @param [PropFile/Pathname] other
249
+ def ==(other)
250
+ if other.class == self.class
251
+ # cache_pathで判定したほうが簡潔に書けるが、キャッシュにファイルが存在しない場合にも判定を行うため、このような実装にしてある
252
+ return version==other.version && basename_prefix==other.basename_prefix
253
+ elsif other.class == Pathname
254
+ return @cache_path==other
255
+ else
256
+ return false
257
+ end
258
+ end
259
+
260
+ # lines_without_commentをstrip_regexpとsplit_regexpで処理した配列を取得
261
+ # @return [Array]
262
+ # @note strip_regexp==/\s+/ の場合であっても、各列の最初と最後の空白しか除去されない。「0000; 1111 2222; 3333;」の、1111と2222の間の空白は除去されない。
263
+ # def values
264
+ def shaped_lines
265
+ return @shaped_lines if @shaped_lines
266
+
267
+ @shaped_lines = []
268
+
269
+ # String#splitはlimit==0(デフォルト)の場合、配列末尾の空文字列が削除される
270
+ # それを防ぐため、limit==-1としてある。これはlimit<0にする事が目的であり、-1という値に意味は無い
271
+ if strip_regexp == /\s+/
272
+ lines_without_comment.each do |line|
273
+ @shaped_lines << line.split(sep=split_regexp, limit=-1)
274
+ .map { _1.gsub(/^\s+/, '') }
275
+ .map { _1.gsub(/\s+$/, '') }
276
+ end
277
+ else
278
+ lines_without_comment.each do |line|
279
+ @shaped_lines << line.split(sep=split_regexp, limit=-1)
280
+ .map { _1.gsub(strip_regexp, '') }
281
+ end
282
+ end
283
+ @shaped_lines
284
+ end
285
+
286
+ # @return [Array] valuesから空の配列を削除したArray
287
+ def netto_shaped_lines
288
+ @netto_shaped_lines ||= shaped_lines.reject { _1.empty? }
289
+ end
290
+
291
+ # 各列に含まれる値(codepointを含む)の配列の配列を取得。
292
+ # @note 返り値の配列のインデックスnは、n列目(最初の列を0列目とする)に含まれるすべての値を含む配列。
293
+ # @return [Array<Set<String>>]
294
+ def contents
295
+ return @contents if @contents
296
+
297
+ @contents = []
298
+
299
+ shaped_lines.each do |shaped_line|
300
+ shaped_line.each_with_index do |col_value, i|
301
+ @contents[i] ||= Set.new
302
+ @contents[i] << Alias.canonical(col_value)
303
+ end
304
+ end
305
+
306
+ # valuesでは区切り文字で区切られたそれぞれの部分を列とみなす。(1行に区切り文字がn個あれば、n+1列あるとみなされる)
307
+ # しかし実際には、最後の区切り文字の右側にコメントしか記述されない事が多いので、その場合は最終列を削除。
308
+ # contentsでは、最後の列に実際に値が無い場合(空orコメントのみの場合)には列とみなさない。
309
+ if @contents[-1].empty? || @contents[-1] == Set.new([""])
310
+ @contents = @contents[...-1]
311
+ end
312
+
313
+ @contents
314
+ end
315
+
316
+ # ファイル内に含まれるすべての値(codepointを含む)を取得。
317
+ # @return [Set<String>]
318
+ def values
319
+ @values ||= contents.reduce(Set.new, :merge)
320
+ end
321
+
322
+ # :nocov:
323
+ # 引数の列がユニーク列(行数に対し、記述されている値の割合が閾値以上の列。Nameプロパティなど、それぞれのcodepointが異なる値を取る傾向にあるプロパティが該当)であるかを判定
324
+ # @param [Integer] column
325
+ # @param [Float] unique_threshold
326
+ # @return [Boolean] ユニーク列であればtrue
327
+ def unique_column?(column, unique_threshold)
328
+ (contents[column].size.to_f / netto_lines.size) > unique_threshold
329
+ end
330
+ # :nocov:
331
+
332
+ # 引数の行・列の値を取得
333
+ # @return [String]
334
+ def value_at(row, column)
335
+ shaped_lines.dig(row, column)
336
+ end
337
+
338
+ # rowの中の1つ以上の列に、propのエイリアスが含まれるかを判定
339
+ # @param [Integer] row 検索する行の番号
340
+ # @param [Property] prop
341
+ def has_property_alias?(row, prop)
342
+ !!shaped_lines[row]&.any? { prop.has_alias?(_1) }
343
+ end
344
+
345
+ # propのエイリアスが含まれる行の範囲を取得
346
+ # @param [Property] prop
347
+ # @return [Array<Range>]
348
+ def property_alias_including_ranges(prop)
349
+ property_alias_including_rows = []
350
+
351
+ lines.size.times do |row|
352
+ property_alias_including_rows << row if has_property_alias?(row, prop)
353
+ end
354
+
355
+ UniPropUtils::RangeProcessor.array_to_ranges(property_alias_including_rows)
356
+ end
357
+
358
+ # rowがコメントのみから成る行かを判定
359
+ # @note 空行もコメント行とみなす
360
+ # @param [Integer] row
361
+ def comment?(row)
362
+ if 0 <= row && row <= lines.size-1
363
+ lines_without_comment[row].match?(/^\s*$/)
364
+ else
365
+ false
366
+ end
367
+ end
368
+
369
+ # コメントのみから成る行の範囲を取得
370
+ # @return [Array<Range>]
371
+ def comment_ranges
372
+ return @comment_ranges if @comment_ranges
373
+
374
+ comment_rows = []
375
+ lines.size.times do |row|
376
+ comment_rows << row if comment?(row)
377
+ end
378
+ @comment_ranges = UniPropUtils::RangeProcessor.array_to_ranges(comment_rows)
379
+
380
+ @comment_ranges
381
+ end
382
+
383
+ # missing valueについて記述された行のみを取得
384
+ # @return [Array<String>]
385
+ def missing_value_lines
386
+ @missing_value_lines ||= lines.filter { _1.match?(/@missing/) }
387
+ end
388
+
389
+ # 空行・コメントのみ以外の行の範囲を取得
390
+ # @return [Array<Range>]
391
+ def information_containing_ranges
392
+ UniPropUtils::RangeProcessor.sub(Range.new(0, lines.size-1), comment_ranges)
393
+ end
394
+
395
+ # row, columnの値がprop.property_value_typeの型にマッチする値かを判定
396
+ # @param [Property] prop
397
+ # @param [Integer] row
398
+ # @param [Integer] column
399
+ # @return [Boolean]
400
+ # @note Miscellaneousプロパティの判定方法はsettings.rbで指定可能
401
+ def property_value_type_match?(row, column, prop)
402
+ if prop.property_value_type == :miscellaneous
403
+ return type_match?(row, column, prop.miscellaneous_format, prop)
404
+ else
405
+ return type_match?(row, column, prop.property_value_type, prop)
406
+ end
407
+ end
408
+
409
+ # row, columnの値がtypeの型にマッチする値かを判定
410
+ # @param [Integer] row
411
+ # @param [Integer] column
412
+ # @param [Symbol] type
413
+ # @param [Property] prop
414
+ # @return [Boolean]
415
+ def type_match?(row, column, type, prop)
416
+ # データファイルには、一番右の;の右側に情報が記述されるフォーマットと、コメントのみが記述されるフォーマットが存在
417
+ # row行目だけを取り出し、一番右側の列が値を持たない(value_atがnil)場合、たまたまrow行目に値が記述されていない(値が空文字列)だけか、ファイル全体として一番右の列に値が記述されていないのか、判定不可能
418
+ # そのため、ここでは値が存在しない列に対しては、空文字列を値として持つと仮定して判定を行う
419
+ value = value_at(row, column) || ""
420
+
421
+ case type
422
+ when :catalog, :enumerated
423
+ return UniPropUtils::TypeJudgementer.validate_enumerative(value, prop)
424
+ when :binary
425
+ return UniPropUtils::TypeJudgementer.validate_binary(value, prop)
426
+ when :string
427
+ return UniPropUtils::TypeJudgementer.validate_string(value)
428
+ when :numeric
429
+ return UniPropUtils::TypeJudgementer.validate_numeric(value)
430
+ when :jamo_short_name
431
+ # Jamo_Short_Nameは、プロパティ値のエイリアス1つか、空文字列(missing)を値として取る(15.0.0でコードポイント110Bの値が空文字列として明示的に記述されている)
432
+ return prop.property_values.any? { _1.has_alias?(value) } || value.empty?
433
+ when :script_extensions
434
+ # Script_Extensionsは1つ以上のScriptプロパティのプロパティ値を取る。2つ以上取る場合、ファイル内では半角スペース区切りで記述される。
435
+ return value.split.all? { version.find_property("Script").has_property_value?(_1) }
436
+ when :text
437
+ # 任意の文字列(空文字列も含む)である事を表すtextでは常にtrueを返す
438
+ return true
439
+ else
440
+ return false
441
+ end
442
+ end
443
+
444
+ # column列目の中で、propが取りうる値の範囲を取得
445
+ # @param [Integer] column
446
+ # @param [Property] prop
447
+ # @return [Array<Range>]
448
+ def property_value_type_match_ranges(column, prop)
449
+ # miscellanesou_format==unqueの場合、ファイル内の全範囲をreturn
450
+ if prop.property_value_type==:miscellaneous && prop.miscellaneous_format==:unique
451
+ if unique_column?(column, prop.unique_threshold)
452
+ return information_containing_ranges
453
+ else
454
+ return []
455
+ end
456
+ end
457
+
458
+ # それ以外の場合、property_value_type_match? がtrueとなる行の範囲をreturn
459
+ property_value_including_rows = []
460
+
461
+ lines.size.times do |row|
462
+ if property_value_type_match?(row, column, prop)
463
+ property_value_including_rows << row
464
+ end
465
+ end
466
+
467
+ UniPropUtils::RangeProcessor.array_to_ranges(property_value_including_rows)
468
+ end
469
+
470
+ # row行目の列数を取得
471
+ # @param [Integer] row
472
+ # @return [Integer]
473
+ def column_size(row)
474
+ shaped_line = shaped_lines[row].to_a
475
+ shaped_line[-1]&.empty? ? shaped_line.size-1 : shaped_line.size
476
+ end
477
+
478
+ # rangeで指定された行の範囲内のうち、最大の列数を取得
479
+ # @param [Range<Integer>]
480
+ # @return [Integer]
481
+ def max_column_size(range)
482
+ range.map{column_size(_1)}.max
483
+ end
484
+
485
+ # property_value_type_match_rangesの最小値を下限、最大値を上限とする範囲の中で、空行、コメント行、column列目の値がpropの行のいずれかに該当する範囲を取得
486
+ # @return [Array<Range<Integer>>]
487
+ def verbose_property_value_type_match_ranges(column, prop)
488
+ prop_ranges = property_value_type_match_ranges(column, prop)
489
+
490
+ if prop_ranges.empty?
491
+ return prop_ranges
492
+ else
493
+ prop_begin_col = UniPropUtils::RangeProcessor.min(prop_ranges)
494
+ prop_end_col = UniPropUtils::RangeProcessor.max(prop_ranges)
495
+
496
+ return UniPropUtils::RangeProcessor.sum_up(
497
+ comment_ranges.map { UniPropUtils::RangeProcessor.cut_external(_1, prop_begin_col, prop_end_col) }.compact + prop_ranges
498
+ )
499
+ end
500
+ end
501
+
502
+ # 修正済みメタデータを参照し、ファイル内に含まれるプロパティを取得
503
+ # @return [Set<Property>]
504
+ def actual_properties
505
+ @actual_properties ||= version.version_metadata.propfile_to_actual_properties[self]
506
+ end
507
+
508
+ # @return [Array<Array<String>>]
509
+ def shaped_missing_value_lines
510
+ @shaped_missing_value_lines ||= missing_value_lines.map {
511
+ _1.gsub(/\s/, '')
512
+ .split(/;/)
513
+ }
514
+ end
515
+
516
+ # @return [PropFileValueGroup]
517
+ def propfile_value_group
518
+ @propfile_value_group ||= PropFileValueGroup.new(self)
519
+ end
520
+
521
+ def is_derived?
522
+ basename_prefix.start_with?(/Derived/)
523
+ end
524
+
525
+ class PropertyAliases < self
526
+ def is_meta_file?() true; end
527
+
528
+ # PropertyAliasesを解析し、タイプとプロパティの関係を取得
529
+ # @return [Hash<String, Set<Array<String>>>]
530
+ def property_value_type_to_shaped_lines
531
+ return @property_type_to_shaped_lines if @property_type_to_shaped_lines
532
+
533
+ @property_type_to_shaped_lines = Hash.new { |hash, key| hash[key]=Set.new }
534
+ mps = UniPropUtils::FileRegexp.matched_positions(cache_path.read, /#\s*={10,}\n#\s(.+)\sProperties\n#\s*={10,}/)
535
+
536
+ mps.size.times do |i|
537
+ mp = mps[i]
538
+ next_mp = mps[i+1]
539
+
540
+ begin_i = mp[:end_col] + 1
541
+ end_i = next_mp ? next_mp[:begin_col] : lines.size
542
+
543
+ property_type = mp[:match_data][1]
544
+
545
+ (begin_i...end_i).each { @property_type_to_shaped_lines[property_type] << shaped_lines[_1] if !shaped_lines[_1].empty?}
546
+ end
547
+
548
+ @property_type_to_shaped_lines
549
+ end
550
+ end
551
+
552
+ class PropertyValueAliases < self
553
+ def is_meta_file?() true; end
554
+
555
+ # このPropertyValueAliasesに含まれる、プロパティ値のエイリアスの一覧を取得。
556
+ # @return [Set]
557
+ def property_value_aliases
558
+ return @property_value_aliases if @property_value_aliases
559
+
560
+ @property_value_aliases = Set.new
561
+
562
+ contents[1..].each { @property_value_aliases.merge(_1) }
563
+
564
+ @property_value_aliases
565
+ end
566
+ end
567
+
568
+ class UnihanFile < self
569
+ def initialize(cache_path, version, strip_regexp: nil, split_regexp: nil)
570
+ if !strip_regexp || !split_regexp
571
+ # strip_regexp, split_regexpが引数で指定されていない場合、settings.rbの記述を使用
572
+ file_format = version.prop_data.settings.unihan_file_format(version.version_name)
573
+
574
+ strip_regexp ||= file_format[:strip]
575
+ split_regexp ||= file_format[:split]
576
+ end
577
+ super(cache_path, version, strip_regexp: strip_regexp, split_regexp: split_regexp)
578
+ end
579
+
580
+ # Unihanの場合はファイル名とパスが一致せず、Unihan.zipに記述されているため、Unihan.zipをダウンロード・展開
581
+ def download_myself
582
+ UniPropUtils::DownloaderWrapper.download_unihan(version.version_name, version.cache_path.parent)
583
+ UniPropUtils::FileManager.recursive_unzip(version.file_cache_paths)
584
+ end
585
+
586
+ def is_unihan_file?() true end
587
+ end
588
+ end
589
+
590
+ class Version
591
+ include Comparable
592
+ attr_accessor :directory, :cache_path
593
+ attr_reader :major, :minor, :tiny, :prop_data, :version_name, :unicode_beta, :excluded_extensions, :excluded_directories, :excluded_files, :included_files, :property_aliases_file_name, :property_value_aliases_file_name
594
+
595
+ def initialize(prop_data, version_name)
596
+ @prop_data = prop_data
597
+ @version_name = version_name
598
+ @major, @minor, @tiny = self.class.parse(@version_name).values
599
+ @cache_path = @prop_data.cache_path + Pathname.new(@version_name)
600
+ @unicode_beta = @prop_data.settings.unicode_beta(@version_name)
601
+ @excluded_extensions = @prop_data.settings.excluded_extensions(@version_name).map { _1.downcase }
602
+ @excluded_directories = @prop_data.settings.excluded_directories(@version_name).map { _1.downcase }
603
+ @excluded_files = @prop_data.settings.excluded_files(@version_name).map { _1.downcase }
604
+ @included_files = @prop_data.settings.included_files(@version_name).map { _1.downcase }
605
+ @property_aliases_file_name = "propertyaliases"
606
+ @property_value_aliases_file_name = "propertyvaluealiases"
607
+ end
608
+
609
+ # バージョン名を対応するx.y.z形式に変換する
610
+ # @param [String] version_name
611
+ # @return [Hash<Symbol,Integer>] Symbolはmajor,minor,tiny
612
+ def self.parse(version_name)
613
+ case version_name
614
+ when /^(\d+)\.(\d+)\.(\d+)$/, /^(\d+)\.(\d+)-Update(\d+)$/
615
+ return {major: $1.to_i, minor: $2.to_i, tiny: $3.to_i}
616
+ when /^(\d+)\.(\d+)-Update$/
617
+ return {major: $1.to_i, minor: $2.to_i, tiny: 0}
618
+ else
619
+ raise ParseError
620
+ end
621
+ end
622
+
623
+ # バージョン名からweightを算出
624
+ # @param [String] version_name
625
+ # @return [Integer]
626
+ def self.name_to_weight(version_name)
627
+ parsed_version_name = parse(version_name)
628
+ parsed_version_name[:major]*10000 + parsed_version_name[:minor]*100 + parsed_version_name[:tiny]
629
+ end
630
+
631
+ # Versionに含まれるファイルのうち、settings.rbの記述に一致するファイルを全件unicode.orgからダウンロード
632
+ def download_version_files(since: true)
633
+ UniPropUtils::DownloaderWrapper.download_version(version_name, cache_path.parent, excluded_extensions, excluded_directories, excluded_files, included_files, unicode_beta: unicode_beta, since: since)
634
+ end
635
+
636
+ # ファイル名を指定してversionのファイルをダウンロード
637
+ # @param [String] file_name Unicodeファイルのbasename_prefixに一致するファイル名
638
+ def download_file(file_name, since: true)
639
+ if UniPropUtils::FileManager.unihan_file?(file_name)
640
+ UniPropUtils::DownloaderWrapper.download_unihan(version_name, cache_path.parent, unicode_beta: unicode_beta, since: since)
641
+ UniPropUtils::FileManager.recursive_unzip(file_cache_paths)
642
+ else
643
+ UniPropUtils::DownloaderWrapper.unicode_basename_download(file_name, version_name, cache_path.parent, unicode_beta: unicode_beta, since: since)
644
+ end
645
+ end
646
+
647
+ # Versionに含まれるPropFile一覧を取得する
648
+ # @param [Boolean] reconfirm unicode.orgからファイルをダウンロードする処理は最初の1回のみ行われ、2回目以降はローカルキャッシュを参照するが、reconfirm==trueの場合、ローカルキャッシュの参照は行わず、再度unicode.orgからファイルをダウンロードする。
649
+ # @return [Set<PropFile>]
650
+ def files(reconfirm: false, since: true, reload: false)
651
+ if @files && !reconfirm && !reload
652
+ return @files
653
+ end
654
+
655
+ if !cache_path.exist? || reconfirm
656
+ download_version_files(since: since)
657
+ end
658
+
659
+ @files = cache_files(since: since, reload: reload)
660
+ @files
661
+ end
662
+
663
+ # キャッシュに保存されているファイルを取得
664
+ # @note キャッシュに該当するディレクトリが存在しない場合、空のSetが返る
665
+ # @param [Boolean] reconfirm unicode.orgにアクセスし、キャッシュのファイルを全て最新バージョンに更新する
666
+ # @param [Boolean] reload trueの場合、メモ化した値を使用せず、キャッシュを再読み込みする
667
+ # @return [Set<PropFile>]
668
+ def cache_files(reconfirm: false, since: true, reload: false)
669
+ return @cache_files if @cache_files && !reconfirm && !reload
670
+
671
+ # キャッシュを最新バージョンに更新
672
+ if reconfirm
673
+ cache_files(since: since).each { download_file(_1.basename_prefix, since: since)}
674
+ end
675
+
676
+ # Unihan.zipを展開
677
+ UniPropUtils::FileManager.recursive_unzip(file_cache_paths)
678
+
679
+ @cache_files = Set.new
680
+
681
+ file_cache_paths.each do |path|
682
+ # 4.1.0ではUnihan.zipの中のUnihan.txtと、そうでないUnihan.txtが存在し、ファイルの内容は同一
683
+ # そのような場合に対処するため、basename_prefixが同一のPropFileオブジェクトが既に作成されている場合、オブジェクトの作成を行わない
684
+ next if @cache_files.any? { _1.basename_prefix==UniPropUtils::FileManager.prefix(path) }
685
+
686
+ propfile = create_propfile(path)
687
+ @cache_files << propfile if propfile
688
+ end
689
+
690
+ @cache_files
691
+ end
692
+
693
+ # cache_pathに保存されているファイルのうち、settings.rbで使用する事にされているファイルのパスを取得
694
+ # @note プログラム実行中にキャッシュの内容は変更されるため、メモ化は行わず、都度探索を行う
695
+ # @return [Array<Pathname>]
696
+ def file_cache_paths
697
+ UniPropUtils::FileManager.filter_file(cache_path.glob('**/*'), excluded_extensions, excluded_directories, excluded_files, included_files)
698
+ end
699
+
700
+ # settings.rbの内容を考慮しながらPathnameオブジェクトからPropFileオブジェクトを作成
701
+ # @param [Pathname] file_path
702
+ # @return [PropFile]
703
+ def create_propfile(path)
704
+ return if UniPropUtils::FileManager.ext_no_dot(path).downcase != "txt"
705
+
706
+ # pathがPropertyAliases.txt, PropertyValueAliases.txtの場合、それらのクラスのインスタンスをreturn
707
+ if UniPropUtils::FileManager.prefix(path).downcase == property_aliases_file_name
708
+ return property_aliases_file
709
+ elsif UniPropUtils::FileManager.prefix(path).downcase == property_value_aliases_file_name
710
+ return property_value_aliases_file
711
+
712
+ # UnihanのファイルにはUnihanFileのインスタンスをreturn
713
+ elsif UniPropUtils::FileManager.unihan_file?(path, unihan_file_names)
714
+ return PropFile::UnihanFile.new(path, self)
715
+ else
716
+ return PropFile.new(path, self)
717
+ end
718
+ end
719
+
720
+ # file_nameに対応するファイルのキャッシュのパスを取得
721
+ # @param [String] file_name Unicodeファイルのprefixに一致するファイル名
722
+ # @return [Pathname] file_nameに対応するキャッシュのローカルのパス
723
+ # @raise [FileNotFoundError] ファイルがキャッシュに存在しない場合発生
724
+ def find_cache_file_path(file_name)
725
+ path = file_cache_paths.find { Alias.canonical(UniPropUtils::FileManager.prefix(_1)) == Alias.canonical(file_name) }
726
+
727
+ if path
728
+ return path
729
+ else
730
+ raise(FileNotFoundError, "#{file_name} has not yet been downloaded.")
731
+ end
732
+ end
733
+
734
+ # キャッシュにfile_nameが表すファイルが存在するかを判定
735
+ def has_cache_file?(file_name)
736
+ return !!find_cache_file_path(file_name)
737
+ rescue
738
+ false
739
+ end
740
+
741
+ # ファイル名/ファイルパスを指定してバージョン内のPropFileオブジェクトを取得
742
+ # @param [String/Pathname] propfile
743
+ # @param [Boolean] confirm trueの場合、ファイルが存在しない際にUnicode.orgからのダウンロードを試みる
744
+ # @raise [FileNotFoundError] ファイルが存在しない場合に発生
745
+ # @return [PropFile]
746
+ def find_file(propfile, confirm: true)
747
+ if propfile.class==String
748
+ file = files.find { |f| Alias.canonical(f.basename_prefix) == Alias.canonical(UniPropUtils::FileManager.prefix(propfile)) }
749
+ elsif propfile.class==Pathname
750
+ file = files.find { |f| f==propfile }
751
+ end
752
+
753
+ if file
754
+ return file
755
+ else
756
+ if confirm==true
757
+ if propfile.class==Pathname
758
+ propfile = propfile.basename
759
+ end
760
+ download_file(UniPropUtils::FileManager.prefix(propfile))
761
+ # ダウンロード後、キャッシュを再読み込みして再度検索を行う
762
+ files(reload: true)
763
+ return find_file(propfile, confirm: false)
764
+ end
765
+
766
+ raise(FileNotFoundError, "#{propfile} is not found.")
767
+ end
768
+ end
769
+
770
+ # @param [String/Pathname] propfile
771
+ def has_file?(propfile)
772
+ !!find_file(propfile)
773
+ rescue
774
+ false
775
+ end
776
+
777
+ # ProeprtyAliasesに該当するPropFileを取得
778
+ # @return [PropertyAliases]
779
+ def property_aliases_file
780
+ return @property_aliases_file if @property_aliases_file
781
+
782
+ if !has_cache_file?(property_aliases_file_name)
783
+ download_file(property_aliases_file_name)
784
+ end
785
+
786
+ property_aliases_file_path = find_cache_file_path(property_aliases_file_name)
787
+
788
+ if property_aliases_file_path
789
+ @property_aliases_file = PropFile::PropertyAliases.new(property_aliases_file_path, self)
790
+ end
791
+
792
+ @property_aliases_file
793
+ end
794
+
795
+ # ProeprtyValueAliasesに該当するPropFileを取得
796
+ # @return [PropertyValueAliases]
797
+ def property_value_aliases_file
798
+ return @property_value_aliases_file if @property_value_aliases_file
799
+
800
+ if !has_cache_file?(property_value_aliases_file_name)
801
+ download_file(property_value_aliases_file_name)
802
+ end
803
+
804
+ property_value_aliases_file_path = find_cache_file_path(property_value_aliases_file_name)
805
+
806
+ if property_value_aliases_file_path
807
+ @property_value_aliases_file = PropFile::PropertyAliases.new(property_value_aliases_file_path, self)
808
+ end
809
+
810
+ @property_value_aliases_file
811
+ end
812
+
813
+ # PropertyAliasesに記述されているProperty一覧を取得
814
+ # @note exclude_unihan==falseの場合であっても、PropertyAliasesに記述されていないUnihanのプロパティは取得されない
815
+ # @param [Boolean] exclude_unihan trueの場合、Unihanのプロパティを除外
816
+ # @return [Set<Property>]
817
+ def properties(exclude_unihan: false)
818
+ if !@properties
819
+ @properties = Set.new
820
+
821
+ # PropertyAliasesをもとに、全プロパティのPropertyオブジェクトを作成
822
+ property_aliases_file.property_value_type_to_shaped_lines.each do |property_value_type, shaped_lines|
823
+ shaped_lines.each do |shaped_line|
824
+ new_prop = Property.new(self, *shaped_line)
825
+ new_prop.property_value_type = property_value_type.downcase
826
+ @properties << new_prop
827
+ end
828
+ end
829
+ end
830
+
831
+ if exclude_unihan
832
+ return @properties - unihan_properties
833
+ else
834
+ return @properties + unihan_properties
835
+ end
836
+ end
837
+
838
+ def has_unihan?
839
+ unihan_files.size!=0
840
+ end
841
+
842
+ # @return [UnihanProp]
843
+ def unihanprop
844
+ @unihanprop ||= UnihanProp.new(unihan_files)
845
+ end
846
+
847
+ # return [Array<Property>]
848
+ def unihan_properties
849
+ unihanprop.unihan_properties
850
+ end
851
+
852
+ # @return [Hash<Property, Array<PropertyValue>>]
853
+ def property_to_property_values
854
+ return @property_to_property_values if @property_to_property_values
855
+
856
+ @property_to_property_values = Hash.new { |hash, key| hash[key]=[] }
857
+
858
+ property_value_aliases_file.netto_shaped_lines.each do |shaped_line|
859
+ prop = find_property(shaped_line[0])
860
+ @property_to_property_values[prop] << PropertyValue.new(prop, *shaped_line[1..])
861
+ end
862
+
863
+ @property_to_property_values
864
+ end
865
+
866
+ # Version内に存在する、property_nameをaliasとして持つPropertyオブジェクトを取得
867
+ # @param [String/Property] property
868
+ # @return [Property]
869
+ # @raise [PropertyNotFoundError] プロパティが存在しない場合に発生
870
+ def find_property(property)
871
+ if property.class==String
872
+ prop = properties.find { _1.has_alias?(property) }
873
+ elsif property.class==Property
874
+ # エイリアス名が長いほど、正しい答えを得られる可能性が高い
875
+ # エイリアス名が短いほど、複数のプロパティが同じエイリアス名を持っている可能性が高い
876
+ property.aliases.sort_by{ _1.size }.reverse_each do |prop_alias|
877
+ return find_property(prop_alias) if has_property?(prop_alias)
878
+ end
879
+ end
880
+
881
+ if prop
882
+ return prop
883
+ else
884
+ raise PropertyNotFoundError.new(property)
885
+ end
886
+ end
887
+
888
+ # @param [Property/String] prop
889
+ def has_property?(prop)
890
+ !!find_property(prop)
891
+ rescue
892
+ false
893
+ end
894
+
895
+ # @param [String] property_name
896
+ # @param [String/Property] property
897
+ # @return [Property]
898
+ # @raise [PropertyNotFoundError] プロパティが存在しない場合に発生
899
+ def find_unihan_property(property)
900
+ if property.class==String
901
+ prop = unihan_properties.find { _1.has_alias?(property) }
902
+ elsif property.class==Property
903
+ # エイリアス名が長いほど、正しい答えを得られる可能性が高い
904
+ # エイリアス名が短いほど、複数のプロパティが同じエイリアス名を持っている可能性が高い
905
+ property.aliases.sort_by{ _1.size }.reverse_each do |prop_alias|
906
+ return find_unihan_property(prop_alias) if has_unihan_property?(prop_alias)
907
+ end
908
+ end
909
+
910
+ if prop
911
+ return prop
912
+ else
913
+ raise PropertyNotFoundError.new(property)
914
+ end
915
+ end
916
+
917
+ # @param [String/Property] property
918
+ def has_unihan_property?(property)
919
+ !!find_unihan_property(property)
920
+ rescue
921
+ false
922
+ end
923
+
924
+ # contentを対応するPropertyオブジェクトに変換して返す。対応するPropertyオブジェクトが無い場合にはnilを返す。contentがArrayの場合、再帰的に変換を実行。
925
+ # @param [String/Array<String>] content
926
+ # @return [Property?/Array<Property?>]
927
+ def convert_property(content)
928
+ if content.class==Array
929
+ return content.map { convert_property(_1) }
930
+ else
931
+ return find_property(content) rescue nil
932
+ end
933
+ end
934
+
935
+ # @return [Array<Property>] Versionに含まれるUnihanファイル
936
+ def unihan_files
937
+ @unihan_files ||= files.filter { _1.is_unihan_file? }
938
+ end
939
+
940
+ # @return [Array<String>?] settings.rbに記述されているUnihanファイル名
941
+ def unihan_file_names
942
+ return @unihan_file_names if @unihan_file_names
943
+
944
+ # キャッシュにUnihanファイルが無い場合、ダウンロードを試みる
945
+ if file_cache_paths.all? { !UniPropUtils::FileManager.unihan_file?(_1) }
946
+ begin
947
+ UniPropUtils::DownloaderWrapper.download_unihan(version_name, cache_path.parent)
948
+ UniPropUtils::FileManager.recursive_unzip(file_cache_paths)
949
+ rescue FileNotFoundError
950
+ # Unicode.orgの対象バージョンにもUnihan.zip, Unihan.txtが存在しない場合(FileNotFoundError)は処理を継続
951
+ # downloader.rbに関する例外などはrescueしない
952
+ end
953
+ end
954
+
955
+ @unihan_file_names = Set.new
956
+ file_cache_paths.each { @unihan_file_names << UniPropUtils::FileManager.prefix(_1) if UniPropUtils::FileManager.unihan_file?(_1) }
957
+ @unihan_file_names = @unihan_file_names.to_a
958
+ @unihan_file_names
959
+ end
960
+
961
+ # @return [VersionMetadata]
962
+ # @raise [MetadataNotFoundError] Versionに対応するメタデータが存在しない場合に発生
963
+ def version_metadata
964
+ @version_metadata ||= VersionMetaData.new(self, prop_data.metadata.find_raw_version_metadata(version_name))
965
+ end
966
+
967
+ def has_version_metadata?
968
+ !!version_metadata
969
+ rescue
970
+ false
971
+ end
972
+
973
+ # settings.rbのPROPERTIES_INFORMATIONのmiscellaneous_formatsを、Propertyオブジェクトをキーとして整理する
974
+ # @note settings.rbに定義が無いプロパティをキーに指定すると、空のハッシュを返す
975
+ # @return [Hash<Property,Hash<Symbol,String>>]
976
+ def property_to_miscellaneous_formats
977
+ return @property_to_miscellaneous_formats if @property_to_miscellaneous_formats
978
+ @property_to_miscellaneous_formats = Hash.new { |hash,key| hash[key]={} }
979
+
980
+ properties.each do |prop|
981
+ prop.uncanonicaled_aliases.each do |als|
982
+ fmt = prop_data.settings.miscellaneous_format(version_name, als)
983
+
984
+ if fmt
985
+ @property_to_miscellaneous_formats[prop] = fmt
986
+ break
987
+ end
988
+ end
989
+ end
990
+
991
+ @property_to_miscellaneous_formats
992
+ end
993
+
994
+ def <=>(other) weight <=> other.weight end
995
+
996
+ def weight() major*10000 + minor*100 + tiny end
997
+ end
998
+ end