cw 0.2.2 → 0.2.4
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/.gitignore +3 -3
- data/.travis.yml +1 -1
- data/README.md +16 -40
- data/Rakefile +4 -0
- data/VERSION +1 -0
- data/bin/cw +44 -0
- data/cw.gemspec +8 -5
- data/data/text/{adv_of_sh_holmes.txt → book.txt} +4 -4
- data/example.rb +17 -17
- data/lib/cw.rb +53 -18
- data/lib/cw/alphabet.rb +37 -0
- data/lib/cw/audio_player.rb +31 -3
- data/lib/cw/book.rb +14 -7
- data/lib/cw/book_details.rb +2 -2
- data/lib/cw/cl.rb +11 -2
- data/lib/cw/config_file.rb +69 -0
- data/lib/cw/cw_dsl.rb +19 -57
- data/lib/cw/monitor_keys.rb +1 -1
- data/lib/cw/params.rb +104 -0
- data/lib/cw/print.rb +19 -23
- data/lib/cw/repeat_word.rb +2 -2
- data/lib/cw/reveal.rb +47 -0
- data/lib/cw/sentence.rb +22 -2
- data/lib/cw/test_words.rb +1 -1
- data/lib/cw/tester.rb +17 -4
- data/lib/cw/timing.rb +1 -1
- data/lib/cw/tone_generator.rb +2 -2
- data/lib/cw/words.rb +20 -1
- data/test/test_cw.rb +8 -11
- data/test/test_tester.rb +62 -0
- metadata +30 -14
- data/classes.svg +0 -145
- data/classes.svg.dot +0 -360
- data/daily.rb +0 -182
- data/data/text/progress.txt +0 -1
- data/lib/cw/cw_params.rb +0 -50
data/lib/cw/alphabet.rb
CHANGED
@@ -4,24 +4,61 @@
|
|
4
4
|
|
5
5
|
class Alphabet
|
6
6
|
|
7
|
+
# initialize class Alphabet
|
8
|
+
# == Parameters:
|
9
|
+
# options::
|
10
|
+
# An optional hash containg options:
|
11
|
+
# :reverse - reverse alphabet
|
12
|
+
# :shuffle - shuffle alphabet
|
13
|
+
# :include - include only these letters of the alphabet
|
14
|
+
# :exclude - exclude these letters from the alphabet
|
15
|
+
|
7
16
|
def initialize(options = {})
|
8
17
|
@options = options
|
9
18
|
end
|
10
19
|
|
20
|
+
# return string containing alphabet
|
21
|
+
|
11
22
|
def alphabet
|
12
23
|
'abcdefghijklmnopqrstuvwxyz'
|
13
24
|
end
|
14
25
|
|
26
|
+
# reverse alphabet if :reverse option defined
|
27
|
+
|
15
28
|
def reverse_alphabet_maybe
|
16
29
|
@letters.reverse! if @options[:reverse]
|
17
30
|
end
|
18
31
|
|
32
|
+
# shuffle alphabet if :shuffle option defined
|
33
|
+
|
19
34
|
def shuffle_alphabet_maybe
|
20
35
|
@letters = @letters.split('').shuffle.join if @options[:shuffle]
|
21
36
|
end
|
22
37
|
|
38
|
+
# include letters passed in as string if :include defined
|
39
|
+
|
40
|
+
def include_letters
|
41
|
+
if @options[:include]
|
42
|
+
@letters = @options[:include]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# exclude letters passed in as string if :exclude defined
|
47
|
+
|
48
|
+
def exclude_letters
|
49
|
+
if @options[:exclude]
|
50
|
+
@letters.tr!(@options[:exclude],'')
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# generate alphabet with options acted upon
|
55
|
+
# == Returns:
|
56
|
+
# alphabet or filtered alphabet
|
57
|
+
|
23
58
|
def generate
|
24
59
|
@letters = alphabet
|
60
|
+
include_letters
|
61
|
+
exclude_letters
|
25
62
|
shuffle_alphabet_maybe
|
26
63
|
reverse_alphabet_maybe
|
27
64
|
@letters.split('').join(' ')
|
data/lib/cw/audio_player.rb
CHANGED
@@ -2,16 +2,43 @@
|
|
2
2
|
|
3
3
|
class AudioPlayer
|
4
4
|
|
5
|
+
AFPLAY = '/usr/bin/afplay'
|
6
|
+
|
7
|
+
#todo def initialize
|
8
|
+
#todo if Params.play_command
|
9
|
+
#todo @play_command = Params.play_command
|
10
|
+
#todo end
|
11
|
+
#todo end
|
12
|
+
|
5
13
|
def tone
|
6
14
|
@tone ||= ToneGenerator.new
|
7
15
|
end
|
8
16
|
|
9
17
|
def play_command
|
10
|
-
@play_command ||=
|
18
|
+
@play_command ||= AFPLAY
|
11
19
|
end
|
12
20
|
|
13
21
|
def play_filename_for_ebook2cw
|
14
|
-
@play_filename ||=
|
22
|
+
@play_filename ||= File.expand_path(Params.audio_filename, audio_dir)
|
23
|
+
end
|
24
|
+
|
25
|
+
def audio_dir
|
26
|
+
Params.audio_dir ||= './audio'
|
27
|
+
end
|
28
|
+
|
29
|
+
def temp_filename_for_ebook2cw
|
30
|
+
File.expand_path("tempxxxx.txt", audio_dir)
|
31
|
+
end
|
32
|
+
|
33
|
+
def convert_book words
|
34
|
+
words = words.delete("\n")
|
35
|
+
File.open(temp_filename_for_ebook2cw, 'w') do |file|
|
36
|
+
file.print words
|
37
|
+
end
|
38
|
+
cl = Cl.new.cl_full(temp_filename_for_ebook2cw)
|
39
|
+
! @dry_run ? `#{cl}` : cl
|
40
|
+
File.delete(temp_filename_for_ebook2cw)
|
41
|
+
File.rename(play_filename_for_ebook2cw + '0000.mp3', play_filename_for_ebook2cw)
|
15
42
|
end
|
16
43
|
|
17
44
|
#FIXME dry_run
|
@@ -19,6 +46,7 @@ class AudioPlayer
|
|
19
46
|
words = words.delete("\n")
|
20
47
|
cl = Cl.new.cl_echo(words)
|
21
48
|
! @dry_run ? `#{cl}` : cl
|
49
|
+
File.rename(play_filename + '0000.mp3', play_filename)
|
22
50
|
end
|
23
51
|
|
24
52
|
def convert_words words
|
@@ -42,7 +70,7 @@ class AudioPlayer
|
|
42
70
|
Process.kill(:TERM, @pid)
|
43
71
|
Process.wait(@pid)
|
44
72
|
rescue
|
45
|
-
puts 'Error: Failed to kill pid ' + @pid.to_s
|
73
|
+
# puts 'Error: Failed to kill pid ' + @pid.to_s
|
46
74
|
exit 1
|
47
75
|
end
|
48
76
|
end
|
data/lib/cw/book.rb
CHANGED
@@ -11,7 +11,6 @@ class Book < FileDetails
|
|
11
11
|
super()
|
12
12
|
read_book book_location
|
13
13
|
find_sentences
|
14
|
-
|
15
14
|
# print_book_advice
|
16
15
|
end
|
17
16
|
|
@@ -37,12 +36,22 @@ class Book < FileDetails
|
|
37
36
|
def audio_play_sentence ; audio.play ; end
|
38
37
|
def print_book_advice ; print.print_advice('Play Book') ; end
|
39
38
|
|
39
|
+
def convert
|
40
|
+
book = @sentence.all.join
|
41
|
+
audio.convert_book(book)
|
42
|
+
end
|
43
|
+
|
40
44
|
def change_or_repeat_sentence?
|
41
45
|
sentence.change_or_repeat?
|
42
46
|
end
|
43
47
|
|
44
48
|
def change_repeat_or_quit?
|
45
|
-
change_or_repeat_sentence? || quit?
|
49
|
+
if(change_or_repeat_sentence? || quit?)
|
50
|
+
sentence.index += 1
|
51
|
+
write_book_progress
|
52
|
+
return true
|
53
|
+
end
|
54
|
+
false
|
46
55
|
end
|
47
56
|
|
48
57
|
def check_sentence_navigation chr
|
@@ -102,7 +111,7 @@ class Book < FileDetails
|
|
102
111
|
end
|
103
112
|
|
104
113
|
def compile_sentence
|
105
|
-
audio.convert_words
|
114
|
+
audio.convert_words(add_space(current_sentence))
|
106
115
|
end
|
107
116
|
|
108
117
|
def compile_and_play
|
@@ -118,12 +127,12 @@ class Book < FileDetails
|
|
118
127
|
|
119
128
|
def next_sentence_or_quit?
|
120
129
|
playing = audio_still_playing?
|
130
|
+
sleep 0.01 if playing
|
121
131
|
next_sentence unless playing
|
122
|
-
sleep 0.01 if playing
|
123
132
|
if change_repeat_or_quit?
|
124
133
|
change_and_kill_audio
|
125
134
|
#todo prn.newline unless quit?
|
126
|
-
true
|
135
|
+
return true
|
127
136
|
end
|
128
137
|
end
|
129
138
|
|
@@ -194,13 +203,11 @@ class Book < FileDetails
|
|
194
203
|
sync_with_audio_player
|
195
204
|
print_words_for_current_sentence
|
196
205
|
print.reset
|
197
|
-
puts
|
198
206
|
end
|
199
207
|
end
|
200
208
|
|
201
209
|
def play_sentences_thread
|
202
210
|
play_sentences_until_quit
|
203
|
-
write_book_progress
|
204
211
|
# kill_threads
|
205
212
|
print "\n\rplay has quit " if @debug
|
206
213
|
end
|
data/lib/cw/book_details.rb
CHANGED
@@ -6,7 +6,7 @@ class BookDetails
|
|
6
6
|
|
7
7
|
HERE = File.dirname(__FILE__) + '/'
|
8
8
|
GEM_BOOK_DIRECTORY = HERE + '../../data/text/'
|
9
|
-
GEM_BOOK_NAME = '
|
9
|
+
GEM_BOOK_NAME = 'book.txt'
|
10
10
|
DEFAULT_BOOK_DIRECTORY = 'books'
|
11
11
|
|
12
12
|
def initialize
|
@@ -36,7 +36,7 @@ class BookDetails
|
|
36
36
|
end
|
37
37
|
|
38
38
|
def book_location
|
39
|
-
File.expand_path(
|
39
|
+
File.expand_path(Params.book_name, Params.book_dir)
|
40
40
|
end
|
41
41
|
|
42
42
|
def arguments args
|
data/lib/cw/cl.rb
CHANGED
@@ -63,7 +63,7 @@ class Cl
|
|
63
63
|
end
|
64
64
|
|
65
65
|
def cl_audio_filename
|
66
|
-
"-o \"#{Params.
|
66
|
+
"-o \"#{File.expand_path(Params.audio_filename, Params.audio_dir)}\" "
|
67
67
|
end
|
68
68
|
|
69
69
|
def coarse_quality(quality)
|
@@ -105,8 +105,17 @@ class Cl
|
|
105
105
|
].collect{|param| param}.join
|
106
106
|
end
|
107
107
|
|
108
|
+
def ebook2cw_path
|
109
|
+
Params.ebook2cw_path ||= 'ebook2cw'
|
110
|
+
Params.ebook2cw_path
|
111
|
+
end
|
112
|
+
|
108
113
|
def cl_echo words
|
109
|
-
"echo #{words} |
|
114
|
+
"echo #{words} | #{ebook2cw_path} #{build_command_line}"
|
115
|
+
end
|
116
|
+
|
117
|
+
def cl_full input
|
118
|
+
"#{ebook2cw_path} #{build_command_line} #{input}"
|
110
119
|
end
|
111
120
|
|
112
121
|
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
class ConfigFile
|
2
|
+
|
3
|
+
CONFIG = ".cw_config"
|
4
|
+
HERE = File.dirname(__FILE__) + '/'
|
5
|
+
CONFIGS = ['wpm', 'book_name', 'book_dir',
|
6
|
+
'play_command', 'success_colour', 'fail_colour',
|
7
|
+
'list_colour', 'ebook2cw_path']
|
8
|
+
|
9
|
+
attr_reader :config
|
10
|
+
|
11
|
+
def config
|
12
|
+
@config ||= Hash.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def readlines f
|
16
|
+
f.readlines
|
17
|
+
end
|
18
|
+
|
19
|
+
def write_config(cfg, line)
|
20
|
+
config[cfg.to_sym] = line.gsub(cfg + ':', '').strip
|
21
|
+
end
|
22
|
+
|
23
|
+
def match_config?(line, cfg)
|
24
|
+
if line
|
25
|
+
tmp = line.strip()[0, cfg.length]
|
26
|
+
return true if(tmp == cfg)
|
27
|
+
end
|
28
|
+
false
|
29
|
+
end
|
30
|
+
|
31
|
+
def extract_config line
|
32
|
+
CONFIGS.each do |cfg|
|
33
|
+
if match_config?(line, cfg + ':')
|
34
|
+
write_config(cfg, line)
|
35
|
+
return
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def read_config
|
41
|
+
File.open(CONFIG,'r') do |f|
|
42
|
+
lines = readlines(f)
|
43
|
+
lines.each do |line|
|
44
|
+
extract_config line
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def read_config_maybe
|
50
|
+
if File.exist?(CONFIG)
|
51
|
+
puts 'Loading config.'
|
52
|
+
read_config
|
53
|
+
config
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def apply_config(sf)
|
58
|
+
cfg = read_config_maybe
|
59
|
+
if cfg
|
60
|
+
cfg.each_pair do |method, arg|
|
61
|
+
begin
|
62
|
+
sf.send("#{method}", arg)
|
63
|
+
rescue
|
64
|
+
end
|
65
|
+
Params.send("#{method}=", arg)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
data/lib/cw/cw_dsl.rb
CHANGED
@@ -2,8 +2,12 @@
|
|
2
2
|
|
3
3
|
# class Cw_dsl provides CW's commands
|
4
4
|
|
5
|
+
require_relative 'params'
|
6
|
+
|
5
7
|
class CwDsl
|
6
8
|
|
9
|
+
include Params::ParamsSetup
|
10
|
+
|
7
11
|
attr_accessor :cl
|
8
12
|
|
9
13
|
HERE = File.dirname(__FILE__) + '/'
|
@@ -16,55 +20,7 @@ class CwDsl
|
|
16
20
|
def initialize
|
17
21
|
@words, @cl, @str =
|
18
22
|
Words.new, Cl.new, Str.new
|
19
|
-
init_config
|
20
|
-
end
|
21
|
-
|
22
|
-
[:name, :wpm,
|
23
|
-
:effective_wpm, :word_spacing,
|
24
|
-
:command_line, :frequency,
|
25
|
-
:author, :title,
|
26
|
-
:quality, :audio_filename,
|
27
|
-
:pause, :noise,
|
28
|
-
:shuffle, :mark_words,
|
29
|
-
:double_words, :single_words,
|
30
|
-
:audio_dir, :def_word_count,
|
31
|
-
:book_name, :book_dir
|
32
|
-
].each do |method|
|
33
|
-
define_method method do |arg = nil|
|
34
|
-
arg ? Params.send("#{method}=", arg) : Params.send("#{method}")
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
[[:pause, :pause, true],
|
39
|
-
[:un_pause, :pause, nil],
|
40
|
-
[:print_letters, :print_letters, true],
|
41
|
-
[:mark_words, :print_letters, nil],
|
42
|
-
[:noise, :noise, true],
|
43
|
-
[:no_noise, :noise, nil],
|
44
|
-
[:shuffle, :shuffle, true],
|
45
|
-
[:no_shuffle, :shuffle, nil],
|
46
|
-
[:double_words, :double_words, true],
|
47
|
-
[:single_words, :double_words, nil],
|
48
|
-
[:use_ebook2cw, :use_ebook2cw, true],
|
49
|
-
[:use_ruby_tone, :use_ebook2cw, nil],
|
50
|
-
].each do |bool|
|
51
|
-
define_method bool[0] do
|
52
|
-
Params.send("#{bool[1]}=", bool[2])
|
53
|
-
@words.shuffle if((bool[1] == :shuffle) && (bool[2]))
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
def init_config
|
58
|
-
Params.config do
|
59
|
-
param :name, :wpm,
|
60
|
-
:dictionary, :command_line, :audio_filename, :tone, :pause,
|
61
|
-
:print_letters, :double_words,
|
62
|
-
:word_filename, :author, :title, :quality, :frequency, :shuffle, :effective_wpm,
|
63
|
-
:max, :min, :word_spacing, :noise, :begin, :end, :word_count, :including,
|
64
|
-
:word_size, :size, :beginning_with, :ending_with, :mark_words, :audio_dir,
|
65
|
-
:use_ebook2cw, :def_word_count, :book_dir, :book_name
|
66
|
-
end
|
67
|
-
|
23
|
+
Params.init_config
|
68
24
|
config_defaults
|
69
25
|
config_files
|
70
26
|
end
|
@@ -75,14 +31,13 @@ class CwDsl
|
|
75
31
|
wpm 25
|
76
32
|
frequency 500
|
77
33
|
dictionary COMMON_WORDS
|
78
|
-
# def_word_count 100
|
79
34
|
}
|
80
35
|
end
|
81
36
|
|
82
37
|
def config_files
|
83
38
|
Params.config {
|
84
39
|
audio_dir 'audio'
|
85
|
-
audio_filename 'audio_output
|
40
|
+
audio_filename 'audio_output'
|
86
41
|
word_filename 'words.txt'
|
87
42
|
}
|
88
43
|
end
|
@@ -123,6 +78,10 @@ class CwDsl
|
|
123
78
|
@words.including letters
|
124
79
|
end
|
125
80
|
|
81
|
+
def containing(* letters)
|
82
|
+
@words.containing letters
|
83
|
+
end
|
84
|
+
|
126
85
|
def no_longer_than(max)
|
127
86
|
Params.max = max
|
128
87
|
@words.no_longer_than max
|
@@ -165,9 +124,9 @@ class CwDsl
|
|
165
124
|
def numbers_spoken()
|
166
125
|
end
|
167
126
|
|
168
|
-
# def add_noise
|
169
|
-
# Params.noise = true
|
170
|
-
# end
|
127
|
+
# def add_noise
|
128
|
+
# Params.noise = true
|
129
|
+
# end
|
171
130
|
|
172
131
|
def reload
|
173
132
|
load_words(Params.dictionary)
|
@@ -191,9 +150,12 @@ class CwDsl
|
|
191
150
|
|
192
151
|
#todo refactor
|
193
152
|
|
194
|
-
def
|
195
|
-
|
196
|
-
end
|
153
|
+
def alpha ; 'a'.upto('z').collect{|ch| ch} ; end
|
154
|
+
|
155
|
+
def vowels ; ['a','e','i','o','u'] ; end
|
156
|
+
def load_vowels ; @words.assign vowels ; end
|
157
|
+
def load_alphabet ; @words.assign alpha ; end
|
158
|
+
def load_consonants ; @words.assign alpha - vowels ; end
|
197
159
|
|
198
160
|
def load_numbers
|
199
161
|
@words.assign '1 2 3 4 5 6 7 8 9 0 '
|
data/lib/cw/monitor_keys.rb
CHANGED
data/lib/cw/params.rb
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Params
|
4
|
+
|
5
|
+
module ParamsSetup
|
6
|
+
|
7
|
+
[:name, :wpm,
|
8
|
+
:effective_wpm, :word_spacing,
|
9
|
+
:command_line, :frequency,
|
10
|
+
:author, :title,
|
11
|
+
:quality, :audio_filename,
|
12
|
+
:pause, :noise,
|
13
|
+
:shuffle, :mark_words,
|
14
|
+
:double_words, :single_words,
|
15
|
+
:audio_dir, :def_word_count,
|
16
|
+
:book_name, :book_dir,
|
17
|
+
:play_command, :success_colour,
|
18
|
+
:fail_colour, :list_colour,
|
19
|
+
:ebook2cw_path
|
20
|
+
].each do |method|
|
21
|
+
define_method method do |arg = nil|
|
22
|
+
arg ? Params.send("#{method}=", arg) : Params.send("#{method}")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
[[:pause, :pause, true],
|
27
|
+
[:un_pause, :pause, nil],
|
28
|
+
[:print_letters, :print_letters, true],
|
29
|
+
[:mark_words, :print_letters, nil],
|
30
|
+
[:noise, :noise, true],
|
31
|
+
[:no_noise, :noise, nil],
|
32
|
+
[:shuffle, :shuffle, true],
|
33
|
+
[:no_shuffle, :shuffle, nil],
|
34
|
+
[:double_words, :double_words, true],
|
35
|
+
[:single_words, :double_words, nil],
|
36
|
+
[:use_ebook2cw, :use_ebook2cw, true],
|
37
|
+
[:use_ruby_tone, :use_ebook2cw, nil],
|
38
|
+
].each do |bool|
|
39
|
+
define_method bool[0] do
|
40
|
+
Params.send("#{bool[1]}=", bool[2])
|
41
|
+
@words.shuffle if((bool[1] == :shuffle) && (bool[2]))
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
extend self
|
48
|
+
|
49
|
+
def init_config
|
50
|
+
config do
|
51
|
+
param :name, :wpm, :dictionary, :command_line, :audio_filename, :tone, :pause,
|
52
|
+
:print_letters, :double_words, :word_filename, :author, :title, :quality,
|
53
|
+
:frequency, :shuffle, :effective_wpm, :max, :min, :word_spacing, :noise,
|
54
|
+
:begin, :end, :word_count, :including, :word_size, :size, :beginning_with,
|
55
|
+
:ending_with, :mark_words, :audio_dir, :use_ebook2cw, :def_word_count,
|
56
|
+
:book_dir, :book_name, :play_command, :success_colour, :fail_colour,
|
57
|
+
:list_colour, :ebook2cw_path
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def param_method values, name
|
62
|
+
value = values.first
|
63
|
+
value ? self.send("#{name}=", value) : instance_variable_get("@#{name}")
|
64
|
+
end
|
65
|
+
|
66
|
+
def param_internal name
|
67
|
+
attr_accessor name
|
68
|
+
instance_variable_set("@#{name}", nil)
|
69
|
+
define_method name do | * values|
|
70
|
+
param_method values, name
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def param( * names)
|
75
|
+
names.each do |name|
|
76
|
+
param_internal name
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def config( & block)
|
81
|
+
instance_eval( & block)
|
82
|
+
end
|
83
|
+
|
84
|
+
def shuffle_str
|
85
|
+
shuffle ? "Shuffle: #{shuffle ? 'yes' : 'no'}\n" : nil
|
86
|
+
end
|
87
|
+
|
88
|
+
def word_count_str
|
89
|
+
word_count ? "Word count: #{word_count}\n" : nil
|
90
|
+
end
|
91
|
+
|
92
|
+
def wpm_str
|
93
|
+
"WPM: #{wpm}\n"
|
94
|
+
end
|
95
|
+
|
96
|
+
def word_size_str
|
97
|
+
size ? "Word size: #{size}\n" : nil
|
98
|
+
end
|
99
|
+
|
100
|
+
def delim_str
|
101
|
+
"#{'=' * Params.name.size}\n"
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|