rroonga 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/NEWS.ja.rdoc +18 -0
- data/NEWS.rdoc +18 -0
- data/bin/grndump +71 -0
- data/ext/groonga/rb-grn-accessor.c +64 -1
- data/ext/groonga/rb-grn-context.c +40 -1
- data/ext/groonga/rb-grn-database.c +47 -5
- data/ext/groonga/rb-grn-index-column.c +77 -7
- data/ext/groonga/rb-grn-object.c +8 -4
- data/ext/groonga/rb-grn-table-cursor.c +56 -1
- data/ext/groonga/rb-grn-table-key-support.c +2 -2
- data/ext/groonga/rb-grn-table.c +10 -38
- data/ext/groonga/rb-grn.h +11 -1
- data/html/index.html +6 -6
- data/html/ranguba.css +8 -1
- data/lib/groonga.rb +1 -0
- data/lib/groonga/dumper.rb +481 -0
- data/lib/groonga/schema.rb +54 -181
- data/rroonga-build.rb +1 -1
- data/test-unit-notify/Rakefile +47 -0
- data/test-unit-notify/lib/test/unit/notify.rb +104 -0
- data/test-unit/COPYING +56 -0
- data/test-unit/GPL +340 -0
- data/test-unit/PSFL +271 -0
- data/test-unit/Rakefile +18 -5
- data/test-unit/html/bar.svg +153 -0
- data/test-unit/html/developer.svg +469 -0
- data/test-unit/html/favicon.ico +0 -0
- data/test-unit/html/favicon.svg +82 -0
- data/test-unit/html/heading-mark.svg +393 -0
- data/test-unit/html/index.html +235 -13
- data/test-unit/html/index.html.ja +258 -15
- data/test-unit/html/install.svg +636 -0
- data/test-unit/html/logo.svg +483 -0
- data/test-unit/html/test-unit.css +339 -0
- data/test-unit/html/tutorial.svg +559 -0
- data/test-unit/lib/test/unit.rb +6 -1
- data/test-unit/lib/test/unit/assertions.rb +115 -11
- data/test-unit/lib/test/unit/autorunner.rb +5 -2
- data/test-unit/lib/test/unit/collector/load.rb +1 -1
- data/test-unit/lib/test/unit/color-scheme.rb +6 -2
- data/test-unit/lib/test/unit/diff.rb +17 -1
- data/test-unit/lib/test/unit/testcase.rb +7 -0
- data/test-unit/lib/test/unit/testresult.rb +34 -2
- data/test-unit/lib/test/unit/ui/console/testrunner.rb +9 -45
- data/test-unit/lib/test/unit/ui/tap/testrunner.rb +2 -12
- data/test-unit/lib/test/unit/ui/testrunner.rb +25 -0
- data/test-unit/lib/test/unit/util/backtracefilter.rb +1 -0
- data/test-unit/lib/test/unit/util/output.rb +31 -0
- data/test-unit/lib/test/unit/version.rb +1 -1
- data/test-unit/test/test-color-scheme.rb +4 -2
- data/test-unit/test/test_assertions.rb +51 -5
- data/test-unit/test/ui/test_tap.rb +33 -0
- data/test-unit/test/util/test-output.rb +11 -0
- data/test/groonga-test-utils.rb +1 -0
- data/test/test-accessor.rb +32 -0
- data/test/test-context.rb +7 -1
- data/test/test-database-dumper.rb +156 -0
- data/test/test-index-column.rb +67 -1
- data/test/test-schema-dumper.rb +181 -0
- data/test/test-schema.rb +53 -97
- data/test/test-table-dumper.rb +83 -0
- metadata +48 -11
- data/ext/groonga/mkmf.log +0 -99
- data/test-unit/html/classic.html +0 -15
data/lib/groonga/schema.rb
CHANGED
@@ -386,9 +386,11 @@ module Groonga
|
|
386
386
|
end
|
387
387
|
end
|
388
388
|
|
389
|
-
#
|
390
|
-
#
|
391
|
-
#
|
389
|
+
# スキーマの内容を文字列をRubyスクリプト形式またはgrn式
|
390
|
+
# 形式で返す。デフォルトはRubyスクリプト形式である。
|
391
|
+
# Rubyスクリプト形式で返された値は
|
392
|
+
# Groonga::Schema.restoreすることによりスキーマ内に組み
|
393
|
+
# 込むことができる。
|
392
394
|
#
|
393
395
|
# dump.rb:
|
394
396
|
# File.open("/tmp/groonga-schema.rb", "w") do |schema|
|
@@ -400,14 +402,36 @@ module Groonga
|
|
400
402
|
# Groonga::Database.create(:path => "/tmp/new-db.grn")
|
401
403
|
# Groonga::Schema.restore(dumped_text)
|
402
404
|
#
|
405
|
+
# grn式形式で返された値はgroongaコマンドで読み込むこと
|
406
|
+
# ができる。
|
407
|
+
#
|
408
|
+
# dump.rb:
|
409
|
+
# File.open("/tmp/groonga-schema.grn", "w") do |schema|
|
410
|
+
# dumped_text = Groonga::Schema.dump(:syntax => :command)
|
411
|
+
# end
|
412
|
+
#
|
413
|
+
# % groonga db/path < /tmp/groonga-schema.grn
|
414
|
+
#
|
403
415
|
# _options_に指定可能な値は以下の通り。
|
404
416
|
#
|
405
417
|
# [+:context+]
|
406
418
|
# スキーマ定義時に使用するGroonga::Contextを指定する。
|
407
419
|
# 省略した場合はGroonga::Context.defaultを使用する。
|
420
|
+
#
|
421
|
+
# [+:syntax+]
|
422
|
+
# スキーマの文字列の形式を指定する。指定可能な値は以
|
423
|
+
# 下の通り。
|
424
|
+
#
|
425
|
+
# [+:ruby+]
|
426
|
+
# Rubyスクリプト形式。省略した場合、+nil+の場合も
|
427
|
+
# Rubyスクリプト形式になる。
|
428
|
+
#
|
429
|
+
# [+:command+]
|
430
|
+
# groongaコマンド形式。groongaコマンドで読み込むこ
|
431
|
+
# とができる。
|
408
432
|
def dump(options={})
|
409
|
-
|
410
|
-
|
433
|
+
schema = new(:context => options[:context],
|
434
|
+
:syntax => options[:syntax])
|
411
435
|
schema.dump
|
412
436
|
end
|
413
437
|
|
@@ -473,10 +497,11 @@ module Groonga
|
|
473
497
|
"mecab" => "TokenMecab",
|
474
498
|
"token_mecab"=> "TokenMecab",
|
475
499
|
}
|
476
|
-
def normalize_type(type) # :nodoc:
|
500
|
+
def normalize_type(type, options={}) # :nodoc:
|
477
501
|
return type if type.nil?
|
478
502
|
return type if type.is_a?(Groonga::Object)
|
479
503
|
type = type.to_s if type.is_a?(Symbol)
|
504
|
+
return type if (options[:context] || Groonga::Context.default)[type]
|
480
505
|
NORMALIZE_TYPE_TABLE[type] || type
|
481
506
|
end
|
482
507
|
end
|
@@ -515,7 +540,8 @@ module Groonga
|
|
515
540
|
# スキーマの内容を文字列で返す。返された値は
|
516
541
|
# Groonga::Schema#restoreすることによりスキーマ内に組み込むことができる。
|
517
542
|
def dump
|
518
|
-
dumper =
|
543
|
+
dumper = SchemaDumper.new(:context => @options[:context],
|
544
|
+
:syntax => @options[:syntax] || :ruby)
|
519
545
|
dumper.dump
|
520
546
|
end
|
521
547
|
|
@@ -1121,7 +1147,7 @@ module Groonga
|
|
1121
1147
|
key_support_table_common = {
|
1122
1148
|
:key_type => normalize_key_type(@options[:key_type] || "ShortText"),
|
1123
1149
|
:key_normalize => @options[:key_normalize],
|
1124
|
-
:default_tokenizer => @options[:default_tokenizer],
|
1150
|
+
:default_tokenizer => normalize_type(@options[:default_tokenizer]),
|
1125
1151
|
}
|
1126
1152
|
|
1127
1153
|
if @table_type == Groonga::Array
|
@@ -1181,7 +1207,9 @@ module Groonga
|
|
1181
1207
|
sub_records = false if sub_records.nil?
|
1182
1208
|
return false unless table.support_sub_records? == sub_records
|
1183
1209
|
path = options[:path]
|
1184
|
-
|
1210
|
+
if path and File.expand_path(table.path) != File.expand_path(path)
|
1211
|
+
return false
|
1212
|
+
end
|
1185
1213
|
|
1186
1214
|
case table
|
1187
1215
|
when Groonga::Array
|
@@ -1189,7 +1217,8 @@ module Groonga
|
|
1189
1217
|
when Groonga::Hash, Groonga::PatriciaTrie
|
1190
1218
|
key_type = normalize_key_type(options[:key_type])
|
1191
1219
|
return false unless table.domain == resolve_name(key_type)
|
1192
|
-
default_tokenizer =
|
1220
|
+
default_tokenizer = normalize_type(options[:default_tokenizer])
|
1221
|
+
default_tokenizer = resolve_name(default_tokenizer)
|
1193
1222
|
return false unless table.default_tokenizer == default_tokenizer
|
1194
1223
|
key_normalize = options[:key_normalize]
|
1195
1224
|
key_normalize = false if key_normalize.nil?
|
@@ -1206,7 +1235,11 @@ module Groonga
|
|
1206
1235
|
end
|
1207
1236
|
|
1208
1237
|
def normalize_key_type(key_type)
|
1209
|
-
|
1238
|
+
normalize_type(key_type || "ShortText")
|
1239
|
+
end
|
1240
|
+
|
1241
|
+
def normalize_type(type)
|
1242
|
+
Schema.normalize_type(type, :context => context)
|
1210
1243
|
end
|
1211
1244
|
|
1212
1245
|
def resolve_name(type)
|
@@ -1250,13 +1283,20 @@ module Groonga
|
|
1250
1283
|
end
|
1251
1284
|
|
1252
1285
|
def define
|
1253
|
-
|
1254
|
-
table = context[@name]
|
1286
|
+
table = removed_table
|
1255
1287
|
dir = columns_directory_path(table)
|
1256
1288
|
result = table.remove
|
1257
1289
|
rmdir_if_dir_exists(dir)
|
1258
1290
|
result
|
1259
1291
|
end
|
1292
|
+
|
1293
|
+
private
|
1294
|
+
def removed_table
|
1295
|
+
context = @options[:context]
|
1296
|
+
table = context[@name]
|
1297
|
+
raise TableNotExists.new(@name) if table.nil?
|
1298
|
+
table
|
1299
|
+
end
|
1260
1300
|
end
|
1261
1301
|
|
1262
1302
|
# スキーマ定義時にGroonga::Schema.create_viewや
|
@@ -1382,7 +1422,7 @@ module Groonga
|
|
1382
1422
|
else
|
1383
1423
|
resolved_type = @type
|
1384
1424
|
end
|
1385
|
-
Schema.normalize_type(resolved_type)
|
1425
|
+
Schema.normalize_type(resolved_type, :context => context)
|
1386
1426
|
end
|
1387
1427
|
|
1388
1428
|
def same_column?(context, column)
|
@@ -1533,172 +1573,5 @@ module Groonga
|
|
1533
1573
|
File.join(columns_dir, name)
|
1534
1574
|
end
|
1535
1575
|
end
|
1536
|
-
|
1537
|
-
class Dumper # :nodoc:
|
1538
|
-
def initialize(options={})
|
1539
|
-
@options = (options || {}).dup
|
1540
|
-
end
|
1541
|
-
|
1542
|
-
def dump
|
1543
|
-
context = @options[:context] || Groonga::Context.default
|
1544
|
-
database = context.database
|
1545
|
-
return nil if database.nil?
|
1546
|
-
|
1547
|
-
header + dump_schema(database) + footer
|
1548
|
-
end
|
1549
|
-
|
1550
|
-
def dump_schema(database)
|
1551
|
-
index_columns = []
|
1552
|
-
reference_columns = []
|
1553
|
-
definitions = []
|
1554
|
-
database.each do |object|
|
1555
|
-
next unless object.is_a?(Groonga::Table)
|
1556
|
-
table = object
|
1557
|
-
schema = create_table_header(table)
|
1558
|
-
table.columns.sort_by {|column| column.local_name}.each do |column|
|
1559
|
-
if column.is_a?(Groonga::IndexColumn)
|
1560
|
-
index_columns << column
|
1561
|
-
else
|
1562
|
-
if column.range.is_a?(Groonga::Table)
|
1563
|
-
reference_columns << column
|
1564
|
-
else
|
1565
|
-
schema << define_column(table, column)
|
1566
|
-
end
|
1567
|
-
end
|
1568
|
-
end
|
1569
|
-
schema << create_table_footer(table)
|
1570
|
-
definitions << schema
|
1571
|
-
end
|
1572
|
-
|
1573
|
-
reference_columns.group_by do |column|
|
1574
|
-
column.table
|
1575
|
-
end.each do |table, columns|
|
1576
|
-
schema = change_table_header(table)
|
1577
|
-
columns.each do |column|
|
1578
|
-
schema << define_reference_column(table, column)
|
1579
|
-
end
|
1580
|
-
schema << change_table_footer(table)
|
1581
|
-
definitions << schema
|
1582
|
-
end
|
1583
|
-
|
1584
|
-
index_columns.group_by do |column|
|
1585
|
-
column.table
|
1586
|
-
end.each do |table, columns|
|
1587
|
-
schema = change_table_header(table)
|
1588
|
-
columns.each do |column|
|
1589
|
-
schema << define_index_column(table, column)
|
1590
|
-
end
|
1591
|
-
schema << change_table_footer(table)
|
1592
|
-
definitions << schema
|
1593
|
-
end
|
1594
|
-
|
1595
|
-
if definitions.empty?
|
1596
|
-
""
|
1597
|
-
else
|
1598
|
-
definitions.join("\n\n") + "\n"
|
1599
|
-
end
|
1600
|
-
end
|
1601
|
-
|
1602
|
-
private
|
1603
|
-
def header
|
1604
|
-
""
|
1605
|
-
end
|
1606
|
-
|
1607
|
-
def footer
|
1608
|
-
""
|
1609
|
-
end
|
1610
|
-
|
1611
|
-
def create_table_header(table)
|
1612
|
-
parameters = []
|
1613
|
-
unless table.is_a?(Groonga::Array)
|
1614
|
-
case table
|
1615
|
-
when Groonga::Hash
|
1616
|
-
parameters << ":type => :hash"
|
1617
|
-
when Groonga::PatriciaTrie
|
1618
|
-
parameters << ":type => :patricia_trie"
|
1619
|
-
end
|
1620
|
-
if table.domain
|
1621
|
-
parameters << ":key_type => #{table.domain.name.dump}"
|
1622
|
-
if table.normalize_key?
|
1623
|
-
parameters << ":key_normalize => true"
|
1624
|
-
end
|
1625
|
-
end
|
1626
|
-
default_tokenizer = table.default_tokenizer
|
1627
|
-
if default_tokenizer
|
1628
|
-
parameters << ":default_tokenizer => #{default_tokenizer.name.dump}"
|
1629
|
-
end
|
1630
|
-
end
|
1631
|
-
parameters << ":force => true"
|
1632
|
-
parameters.unshift("")
|
1633
|
-
parameters = parameters.join(",\n ")
|
1634
|
-
"create_table(#{table.name.dump}#{parameters}) do |table|\n"
|
1635
|
-
end
|
1636
|
-
|
1637
|
-
def create_table_footer(table)
|
1638
|
-
"end"
|
1639
|
-
end
|
1640
|
-
|
1641
|
-
def change_table_header(table)
|
1642
|
-
"change_table(#{table.name.inspect}) do |table|\n"
|
1643
|
-
end
|
1644
|
-
|
1645
|
-
def change_table_footer(table)
|
1646
|
-
"end"
|
1647
|
-
end
|
1648
|
-
|
1649
|
-
def define_column(table, column)
|
1650
|
-
type = column_method(column)
|
1651
|
-
name = column.local_name
|
1652
|
-
" table.#{type}(#{name.inspect})\n"
|
1653
|
-
end
|
1654
|
-
|
1655
|
-
def define_reference_column(table, column)
|
1656
|
-
name = column.local_name
|
1657
|
-
reference = column.range
|
1658
|
-
" table.reference(#{name.dump}, #{reference.name.dump})\n"
|
1659
|
-
end
|
1660
|
-
|
1661
|
-
def define_index_column(table, column)
|
1662
|
-
target_table_name = column.range.name
|
1663
|
-
sources = column.sources
|
1664
|
-
source_names = sources.collect do |source|
|
1665
|
-
if source.is_a?(table.class)
|
1666
|
-
"_key".dump
|
1667
|
-
else
|
1668
|
-
source.local_name.dump
|
1669
|
-
end
|
1670
|
-
end.join(", ")
|
1671
|
-
arguments = [target_table_name.dump,
|
1672
|
-
sources.size == 1 ? source_names : "[#{source_names}]",
|
1673
|
-
":name => #{column.local_name.dump}"]
|
1674
|
-
" table.index(#{arguments.join(', ')})\n"
|
1675
|
-
end
|
1676
|
-
|
1677
|
-
def column_method(column)
|
1678
|
-
range = column.range
|
1679
|
-
case range.name
|
1680
|
-
when "Int32"
|
1681
|
-
"integer32"
|
1682
|
-
when "Int64"
|
1683
|
-
"integer64"
|
1684
|
-
when "UInt32"
|
1685
|
-
"unsigned_integer32"
|
1686
|
-
when "UInt64"
|
1687
|
-
"unsigned_integer64"
|
1688
|
-
when "Float"
|
1689
|
-
"float"
|
1690
|
-
when "Time"
|
1691
|
-
"time"
|
1692
|
-
when "ShortText"
|
1693
|
-
"short_text"
|
1694
|
-
when "Text"
|
1695
|
-
"text"
|
1696
|
-
when "LongText"
|
1697
|
-
"long_text"
|
1698
|
-
else
|
1699
|
-
raise ArgumentError, "unsupported column: #{column.inspect}"
|
1700
|
-
end
|
1701
|
-
end
|
1702
|
-
end
|
1703
1576
|
end
|
1704
1577
|
end
|
data/rroonga-build.rb
CHANGED
@@ -0,0 +1,47 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
|
3
|
+
require 'pathname'
|
4
|
+
|
5
|
+
base_dir = Pathname(__FILE__).dirname.expand_path
|
6
|
+
test_unit_dir = (base_dir.parent + "test-unit").expand_path
|
7
|
+
test_unit_lib_dir = test_unit_dir + "lib"
|
8
|
+
lib_dir = base_dir + "lib"
|
9
|
+
|
10
|
+
$LOAD_PATH.unshift(test_unit_lib_dir.to_s)
|
11
|
+
$LOAD_PATH.unshift(lib_dir.to_s)
|
12
|
+
|
13
|
+
require 'test/unit/notify'
|
14
|
+
|
15
|
+
require 'rubygems'
|
16
|
+
require 'hoe'
|
17
|
+
|
18
|
+
Test::Unit.run = true
|
19
|
+
|
20
|
+
version = Test::Unit::Notify::VERSION
|
21
|
+
ENV["VERSION"] = version
|
22
|
+
Hoe.spec('test-unit-notify') do
|
23
|
+
self.version = version
|
24
|
+
self.rubyforge_name = "test-unit"
|
25
|
+
|
26
|
+
developer('Kouhei Sutou', 'kou@clear-code.com')
|
27
|
+
|
28
|
+
extra_deps << ["test-unit"]
|
29
|
+
end
|
30
|
+
|
31
|
+
task :docs do
|
32
|
+
doc_dir = base_dir + "doc"
|
33
|
+
doc_screenshot_dir = doc_dir + "screenshot"
|
34
|
+
mkdir_p(doc_screenshot_dir.to_s)
|
35
|
+
(base_dir + "screenshot").children.each do |file|
|
36
|
+
next if file.directory?
|
37
|
+
cp(file.to_s, doc_screenshot_dir.to_s)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
task :tag do
|
42
|
+
message = "Released Test::Unit::Notify #{version}!"
|
43
|
+
base = "svn+ssh://#{ENV['USER']}@rubyforge.org/var/svn/test-unit/extensions/test-unit-notify/"
|
44
|
+
sh 'svn', 'copy', '-m', message, "#{base}trunk", "#{base}tags/#{version}"
|
45
|
+
end
|
46
|
+
|
47
|
+
# vim: syntax=Ruby
|
@@ -0,0 +1,104 @@
|
|
1
|
+
#--
|
2
|
+
#
|
3
|
+
# Author:: Kouhei Sutou
|
4
|
+
# Copyright::
|
5
|
+
# * Copyright (c) 2010 Kouhei Sutou <kou@clear-code.com>
|
6
|
+
# License:: Ruby license.
|
7
|
+
|
8
|
+
require 'pathname'
|
9
|
+
require 'erb'
|
10
|
+
require 'test/unit'
|
11
|
+
|
12
|
+
module Test
|
13
|
+
module Unit
|
14
|
+
AutoRunner.setup_option do |auto_runner, options|
|
15
|
+
options.on("--[no-]notify",
|
16
|
+
"Notify test result at the last.") do |use_notify|
|
17
|
+
auto_runner.listeners.reject! do |listener|
|
18
|
+
listener.is_a?(Notify::Notifier)
|
19
|
+
end
|
20
|
+
auto_runner.listeners << Notify::Notifier.new if use_notify
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module Notify
|
25
|
+
VERSION = "0.0.2"
|
26
|
+
|
27
|
+
class Notifier
|
28
|
+
include ERB::Util
|
29
|
+
|
30
|
+
base_dir = Pathname(__FILE__).dirname.parent.parent.parent.expand_path
|
31
|
+
ICON_DIR = base_dir + "data" + "icons"
|
32
|
+
def initialize
|
33
|
+
@theme = "kinotan"
|
34
|
+
end
|
35
|
+
|
36
|
+
def attach_to_mediator(mediator)
|
37
|
+
mediator.add_listener(UI::TestRunnerMediator::STARTED,
|
38
|
+
&method(:started))
|
39
|
+
mediator.add_listener(UI::TestRunnerMediator::FINISHED,
|
40
|
+
&method(:finished))
|
41
|
+
end
|
42
|
+
|
43
|
+
def started(result)
|
44
|
+
@result = result
|
45
|
+
end
|
46
|
+
|
47
|
+
def finished(elapsed_time)
|
48
|
+
case RUBY_PLATFORM
|
49
|
+
when /mswin|mingw|cygwin/
|
50
|
+
# how?
|
51
|
+
when /darwin/
|
52
|
+
# growl?
|
53
|
+
else
|
54
|
+
notify_by_notify_send(elapsed_time)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def notify_by_notify_send(elapsed_time)
|
59
|
+
icon = guess_suitable_icon
|
60
|
+
args = ["notify-send",
|
61
|
+
"--expire-time", "5000",
|
62
|
+
"--urgency", urgency]
|
63
|
+
args.concat(["--icon", icon.to_s]) if icon
|
64
|
+
title = "%s [%g%%] (%gs)" % [@result.status,
|
65
|
+
@result.pass_percentage,
|
66
|
+
elapsed_time]
|
67
|
+
args << title
|
68
|
+
args << h(@result.summary)
|
69
|
+
system(*args)
|
70
|
+
end
|
71
|
+
|
72
|
+
def guess_suitable_icon
|
73
|
+
icon_dir = ICON_DIR + @theme
|
74
|
+
status = @result.status
|
75
|
+
icon_base_names = [status]
|
76
|
+
if @result.passed?
|
77
|
+
icon_base_names << "pass"
|
78
|
+
else
|
79
|
+
case status
|
80
|
+
when "failure"
|
81
|
+
icon_base_names << "error"
|
82
|
+
when "error"
|
83
|
+
icon_base_names << "failure"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
icon_base_names << "default"
|
87
|
+
icon_base_names.each do |base_name|
|
88
|
+
icon = icon_dir + "#{base_name}.png"
|
89
|
+
return icon if icon.exist?
|
90
|
+
end
|
91
|
+
nil
|
92
|
+
end
|
93
|
+
|
94
|
+
def urgency
|
95
|
+
if @result.passed?
|
96
|
+
"normal"
|
97
|
+
else
|
98
|
+
"critical"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|