raheui 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,212 @@
1
+ # encoding: utf-8
2
+ module Raheui
3
+ # Run Aheui code.
4
+ class Runner
5
+ # Point class for execution cursor position and delta of it.
6
+ Point = Struct.new(:x, :y)
7
+
8
+ # Numbers of required elements for each command.
9
+ REQUIRED_STORE_SIZE = [
10
+ 0, 0, 2, 2, 2, 2, 1, 0, 1, 0, # ㄱ ㄲ ㄴ ㄷ ㄸ ㄹ ㅁ ㅂ ㅃ ㅅ
11
+ 1, 0, 2, 0, 1, 0, 2, 2, 0 # ㅆ ㅇ ㅈ ㅉ ㅊ ㅋ ㅌ ㅍ ㅎ
12
+ ]
13
+
14
+ # Delta values of each medial consonant.
15
+ MEDIAL_DELTAS = [
16
+ [1, 0], nil, [2, 0], nil, # ㅏ ㅐ ㅑ ㅒ
17
+ [-1, 0], nil, [-2, 0], nil, # ㅓ ㅔ ㅕ ㅖ
18
+ [0, -1], nil, nil, nil, [0, -2], # ㅗ ㅘ ㅙ ㅚ ㅛ
19
+ [0, 1], nil, nil, nil, [0, 2], # ㅜ ㅝ ㅞ ㅟ ㅠ
20
+ [:+, :-], [:-, :-], [:-, :+] # ㅡ ㅢ ㅣ
21
+ ]
22
+
23
+ # Numbers of strokes of each final consonant.
24
+ FINAL_STROKES = [
25
+ 0, # No final consonant.
26
+ 2, 4, 4, 2, 5, 5, 3, 5, 7, 9, # ㄱ ㄲ ㄳ ㄴ ㄵ ㄶ ㄷ ㄹ ㄺ ㄻ
27
+ 9, 7, 9, 9, 8, 4, 4, 6, 2, 4, # ㄼ ㄽ ㄾ ㄿ ㅀ ㅁ ㅂ ㅄ ㅅ ㅆ
28
+ 1, 3, 4, 3, 4, 4, 3 # ㅇ ㅈ ㅊ ㅋ ㅌ ㅍ ㅎ
29
+ ]
30
+
31
+ private_constant :REQUIRED_STORE_SIZE, :MEDIAL_DELTAS, :FINAL_STROKES
32
+
33
+ # Initialize a Runner. Get a Code instance and initialize Stores.
34
+ #
35
+ # code - The Code instance to execute.
36
+ def initialize(code)
37
+ @code = code
38
+ end
39
+
40
+ # Run the Aheui Code.
41
+ #
42
+ # Returns the Integer exit code.
43
+ def run
44
+ reset
45
+ step until @finished
46
+ @selected_store.pop || 0
47
+ end
48
+
49
+ private
50
+
51
+ # Reset cursor and Stores. Select first Store.
52
+ #
53
+ # Returns nothing.
54
+ def reset
55
+ @cursor = Point.new(0, 0)
56
+ @delta = Point.new(0, 1)
57
+ @stores = Code::FINAL_CONSONANTS.times.map do |consonant|
58
+ case consonant
59
+ when 21 then Queue.new # ㅇ
60
+ when 27 then Port.new # ㅎ
61
+ else Stack.new
62
+ end
63
+ end
64
+ @selected_store = @stores[0]
65
+ @finished = false
66
+ end
67
+
68
+ # Process current character which cursor points to and move cursor.
69
+ #
70
+ # Returns nothing.
71
+ def step
72
+ consonants = @code[@cursor.x, @cursor.y]
73
+ unless consonants.empty?
74
+ initial, medial, final = consonants
75
+ @delta.x, @delta.y = delta(medial)
76
+ if @selected_store.size < REQUIRED_STORE_SIZE[initial]
77
+ @delta.x, @delta.y = -@delta.x, -@delta.y
78
+ else
79
+ process(initial, final)
80
+ end
81
+ end
82
+ move
83
+ end
84
+
85
+ # Process a Korean alphabet.
86
+ #
87
+ # initial - An Integer index of initial consonant of the Korean alphabet.
88
+ # final - An Integer index of final consonant of the Korean alphabet.
89
+ #
90
+ # Returns nothing.
91
+ def process(initial, final)
92
+ case initial
93
+ when 2 # ㄴ
94
+ operate(:/)
95
+ when 3 # ㄷ
96
+ operate(:+)
97
+ when 4 # ㄸ
98
+ operate(:*)
99
+ when 5 # ㄹ
100
+ operate(:%)
101
+ when 6 # ㅁ
102
+ op = @selected_store.pop
103
+ if final == 21 # ㅇ
104
+ IO.print_int(op)
105
+ elsif final == 27 # ㅎ
106
+ IO.print_chr(op)
107
+ end
108
+ when 7 # ㅂ
109
+ op = if final == 21 # ㅇ
110
+ IO.read_int
111
+ elsif final == 27 # ㅎ
112
+ IO.read_chr
113
+ else
114
+ FINAL_STROKES[final]
115
+ end
116
+ @selected_store.push(op)
117
+ when 8 # ㅃ
118
+ @selected_store.push_dup
119
+ when 9 # ㅅ
120
+ @selected_store = @stores[final]
121
+ when 10 # ㅆ
122
+ op = @selected_store.pop
123
+ @stores[final].push(op)
124
+ when 12 # ㅈ
125
+ op1 = @selected_store.pop
126
+ op2 = @selected_store.pop
127
+ @selected_store.push(op2 >= op1 ? 1 : 0)
128
+ when 14 # ㅊ
129
+ op = @selected_store.pop
130
+ @delta.x, @delta.y = -@delta.x, -@delta.y if op == 0
131
+ when 16 # ㅌ
132
+ operate(:-)
133
+ when 17 # ㅍ
134
+ @selected_store.swap
135
+ when 18 # ㅎ
136
+ @finished = true
137
+ end
138
+ end
139
+
140
+ # Helper method for basic operators. +, -, *, / and % can be processed.
141
+ #
142
+ # method - A Symbol method to execute.
143
+ #
144
+ # Examples
145
+ #
146
+ # operate(:+)
147
+ #
148
+ # operate(:-)
149
+ #
150
+ # operate(:*)
151
+ #
152
+ # operate(:/)
153
+ #
154
+ # operate(:%)
155
+ #
156
+ # Returns nothing.
157
+ def operate(method)
158
+ op1 = @selected_store.pop
159
+ op2 = @selected_store.pop
160
+ @selected_store.push([op2, op1].reduce(method))
161
+ end
162
+
163
+ # Get delta x and y position for next move.
164
+ #
165
+ # medial - An Integer index of medial consonant of the Korean alphabet.
166
+ #
167
+ # Examples
168
+ #
169
+ # delta(0)
170
+ # # => [1, 0]
171
+ #
172
+ # Returns an Array of Integer delta x and y position.
173
+ def delta(medial)
174
+ delta = MEDIAL_DELTAS[medial]
175
+ if delta
176
+ x, y = delta
177
+ x = x == :+ ? @delta.x : -@delta.x if x.is_a? Symbol
178
+ y = y == :+ ? @delta.y : -@delta.y if y.is_a? Symbol
179
+ [x, y]
180
+ else
181
+ [@delta.x, @delta.y]
182
+ end
183
+ end
184
+
185
+ # Move cursor to proper position. Wrap the position if it goes to outside of
186
+ # the code.
187
+ #
188
+ # Returns nothing.
189
+ def move
190
+ @cursor.x = wrap(@cursor.x + @delta.x, @code.width)
191
+ @cursor.y = wrap(@cursor.y + @delta.y, @code.height)
192
+ end
193
+
194
+ # Wrap a number to be between 0 and max value excluding max value. If the
195
+ # number is negative, it goes to max - 1. If the number is bigger than or
196
+ # equal to max value, it goes to zero.
197
+ #
198
+ # num - An Integer to be wrapped.
199
+ # max - An Integer max value.
200
+ #
201
+ # Returns an Integer between 0 and max - 1.
202
+ def wrap(num, max)
203
+ if num < 0
204
+ max - 1
205
+ elsif num >= max
206
+ 0
207
+ else
208
+ num
209
+ end
210
+ end
211
+ end
212
+ end
@@ -0,0 +1,13 @@
1
+ # encoding: utf-8
2
+ module Raheui
3
+ # Stack class for Aheui.
4
+ class Stack < Store
5
+ # Delegates push, pop to @store.
6
+ delegate [:push, :pop] => :@store
7
+
8
+ # Swap the last two elements of Stack.
9
+ def swap
10
+ @store[-1], @store[-2] = @store[-2], @store[-1] if size > 1
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,41 @@
1
+ # encoding: utf-8
2
+ module Raheui
3
+ # Base Store class for Aheui. Every child classes should implement push, pop
4
+ # and swap method.
5
+ class Store
6
+ extend Forwardable
7
+
8
+ BASE_METHODS = [:push, :pop, :swap]
9
+ private_constant :BASE_METHODS
10
+
11
+ # Delegates size to @store.
12
+ def_delegator :@store, :size
13
+
14
+ # Initialize a Stack.
15
+ def initialize
16
+ check_base_methods
17
+ @store = []
18
+ end
19
+
20
+ # Push the last element to Store.
21
+ def push_dup
22
+ push(@store.last) if size > 0
23
+ end
24
+
25
+ private
26
+
27
+ # Check whether base methods are implemented.
28
+ #
29
+ # Returns nothing.
30
+ # Raises NotImplementedError if base methods are not implemented.
31
+ def check_base_methods
32
+ errors = []
33
+ BASE_METHODS.each do |method|
34
+ errors << method unless respond_to?(method)
35
+ end
36
+ return if errors.empty?
37
+ fail NotImplementedError, 'base methods are not implemented:' \
38
+ " #{errors.join(', ')}"
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,7 @@
1
+ # encoding: utf-8
2
+ module Raheui
3
+ # Holds the Raheui version information.
4
+ module Version
5
+ STRING = '1.0.0'
6
+ end
7
+ end
data/lib/raheui.rb ADDED
@@ -0,0 +1,16 @@
1
+ # encoding: utf-8
2
+ require 'raheui/version'
3
+
4
+ require 'forwardable'
5
+ require 'raheui/store'
6
+ require 'raheui/stack'
7
+ require 'raheui/queue'
8
+ require 'raheui/port'
9
+
10
+ require 'raheui/io'
11
+ require 'raheui/code'
12
+ require 'raheui/runner'
13
+
14
+ require 'optparse'
15
+ require 'raheui/option'
16
+ require 'raheui/cli'
data/raheui.gemspec ADDED
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'raheui/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'raheui'
8
+ spec.version = Raheui::Version::STRING
9
+ spec.authors = ['ChaYoung You']
10
+ spec.email = ['yousbe@gmail.com']
11
+ spec.summary = 'Aheui interpreter in Ruby.'
12
+ spec.description = 'Aheui interpreter in Ruby.'
13
+ spec.homepage = ''
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(/^bin\//) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(/^(test|spec|features)\//)
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_development_dependency 'bundler', '~> 1.6'
22
+ spec.add_development_dependency 'rake', '~> 10.0'
23
+ spec.add_development_dependency 'rspec', '~> 3.0'
24
+ spec.add_development_dependency 'rubocop', '~> 0.25.0'
25
+ spec.add_development_dependency 'simplecov', '~> 0.9'
26
+ end
@@ -0,0 +1,39 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ describe Raheui::CLI do
5
+ include FileHelper
6
+ include_context 'isolated environment'
7
+
8
+ subject(:cli) { described_class.new }
9
+
10
+ before(:example) { $stdout = StringIO.new }
11
+ after(:example) { $stdout = STDOUT }
12
+
13
+ it 'runs aheui file passed as an argument' do
14
+ create_file('helloworld.aheui', %w(밤밣따빠밣밟따뿌
15
+ 빠맣파빨받밤뚜뭏
16
+ 돋밬탕빠맣붏두붇
17
+ 볻뫃박발뚷투뭏붖
18
+ 뫃도뫃희멓뭏뭏붘
19
+ 뫃봌토범더벌뿌뚜
20
+ 뽑뽀멓멓더벓뻐뚠
21
+ 뽀덩벐멓뻐덕더벅))
22
+ expect(cli.run(['helloworld.aheui'])).to be(0)
23
+ expect($stdout.string).to eq("Hello, world!\n")
24
+ end
25
+
26
+ it 'runs string passed as an stdin argument' do
27
+ allow($stdin).to receive(:read).once
28
+ .and_return(%w(밤밣따빠밣밟따뿌
29
+ 빠맣파빨받밤뚜뭏
30
+ 돋밬탕빠맣붏두붇
31
+ 볻뫃박발뚷투뭏붖
32
+ 뫃도뫃희멓뭏뭏붘
33
+ 뫃봌토범더벌뿌뚜
34
+ 뽑뽀멓멓더벓뻐뚠
35
+ 뽀덩벐멓뻐덕더벅).join("\n"))
36
+ expect(cli.run([])).to be(0)
37
+ expect($stdout.string).to eq("Hello, world!\n")
38
+ end
39
+ end
@@ -0,0 +1,119 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ describe Raheui::Code do
5
+ shared_examples 'a code' do
6
+ subject(:code) { Raheui::Code.new(code_str) }
7
+ let(:consonants) { code.method(:consonants) }
8
+
9
+ it 'has width which is maximum line length of code with minimum value 1' do
10
+ width = code_str.lines.map { |line| line.chomp.size }.max || 1
11
+ expect(code.width).to be(width)
12
+ end
13
+
14
+ it 'has height which is number of lines with minimum value 1' do
15
+ height = [code_str.lines.count, 1].max
16
+ expect(code.height).to be(height)
17
+ end
18
+
19
+ describe '#[]' do
20
+ it 'returns item at given position' do
21
+ code_str.lines.each_with_index do |line, y|
22
+ line.chomp.chars.each_with_index do |ch, x|
23
+ expect(code[x, y]).to match_array(consonants.call(ch))
24
+ end
25
+ end
26
+ end
27
+
28
+ it 'returns item at given negative position' do
29
+ code_str.lines.each_with_index do |line, y|
30
+ max_x = line.chomp.chars.count
31
+ line.chomp.chars.each_with_index do |ch, x|
32
+ expected = consonants.call(ch)
33
+ expect(code[x - max_x, y]).to match_array(expected)
34
+ expect(code[x, y - code.height]).to match_array(expected)
35
+ expect(code[x - max_x, y - code.height]).to match_array(expected)
36
+ end
37
+ end
38
+ end
39
+
40
+ it 'returns an empty array if there is no item at given position' do
41
+ expect(code[0, code.height]).to match_array([])
42
+ expect(code[code.width, 0]).to match_array([])
43
+ expect(code[code.width, code.height]).to match_array([])
44
+ end
45
+ end
46
+ end
47
+
48
+ describe Raheui::Code, 'with empty code' do
49
+ let(:code_str) { '' }
50
+ it_behaves_like 'a code'
51
+ end
52
+
53
+ describe Raheui::Code, 'with aheui code' do
54
+ let(:code_str) { '아희' }
55
+ it_behaves_like 'a code'
56
+ end
57
+
58
+ describe Raheui::Code, 'with hello world code' do
59
+ let(:code_str) do
60
+ <<-CODE
61
+ 밤밣따빠밣밟따뿌
62
+ 빠맣파빨받밤뚜뭏
63
+ 돋밬탕빠맣붏두붇
64
+ 볻뫃박발뚷투뭏붖
65
+ 뫃도뫃희멓뭏뭏붘
66
+ 뫃봌토범더벌뿌뚜
67
+ 뽑뽀멓멓더벓뻐뚠
68
+ 뽀덩벐멓뻐덕더벅
69
+ CODE
70
+ end
71
+
72
+ it_behaves_like 'a code'
73
+ end
74
+
75
+ describe '#consonants' do
76
+ subject { Raheui::Code.new('') }
77
+ let(:consonants) { subject.method(:consonants) }
78
+ before(:example) do
79
+ stub_const('INITIAL_CONSONANTS', Raheui::Code::INITIAL_CONSONANTS)
80
+ stub_const('MEDIAL_CONSONANTS', Raheui::Code::MEDIAL_CONSONANTS)
81
+ stub_const('FINAL_CONSONANTS', Raheui::Code::FINAL_CONSONANTS)
82
+ end
83
+
84
+ it 'returns consonants of Korean alphabet' do
85
+ examples = [
86
+ # 가
87
+ [0, 0, 0],
88
+ # 힣
89
+ [INITIAL_CONSONANTS - 1, MEDIAL_CONSONANTS - 1, FINAL_CONSONANTS - 1]
90
+ ]
91
+ examples.concat(10.times.map do
92
+ [rand(INITIAL_CONSONANTS),
93
+ rand(MEDIAL_CONSONANTS),
94
+ rand(FINAL_CONSONANTS)]
95
+ end.uniq)
96
+ examples.each do |initial, medial, final|
97
+ alphabet = (
98
+ 0xAC00 +
99
+ initial * MEDIAL_CONSONANTS * FINAL_CONSONANTS +
100
+ medial * FINAL_CONSONANTS +
101
+ final
102
+ ).chr(Encoding::UTF_8)
103
+ expect(consonants.call(alphabet)).to match_array(
104
+ [initial, medial, final])
105
+ end
106
+ end
107
+
108
+ it 'returns an empty array for non-Korean alphabet' do
109
+ examples = [
110
+ *0..127,
111
+ 0xAC00 + INITIAL_CONSONANTS * MEDIAL_CONSONANTS * FINAL_CONSONANTS
112
+ ]
113
+ examples.concat(%w(あ 漢   å ★).map(&:ord))
114
+ examples.each do |ch|
115
+ expect(consonants.call(ch)).to match_array([])
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,94 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ describe Raheui::IO do
5
+ describe '.read_int' do
6
+ let(:value) { rand(-100..100) }
7
+
8
+ it { is_expected.to respond_to(:read_int) }
9
+
10
+ it 'reads an integer' do
11
+ allow($stdin).to receive(:gets).with(no_args).once
12
+ .and_return("#{value}\n")
13
+ expect(subject.read_int).to be(value)
14
+ end
15
+ end
16
+
17
+ describe '.read_chr' do
18
+ it { is_expected.to respond_to(:read_chr) }
19
+
20
+ it 'reads an ASCII code' do
21
+ [0, *32..126].each do |value|
22
+ allow($stdin).to receive(:getc).with(no_args).once
23
+ .and_return(value.chr)
24
+ expect(subject.read_chr).to be(value)
25
+ end
26
+ end
27
+
28
+ it 'reads an control character' do
29
+ [*1..31, *127..159].each do |value|
30
+ allow($stdin).to receive(:getc).with(no_args).once
31
+ .and_return(value.chr)
32
+ expect(subject.read_chr).to be(value)
33
+ end
34
+ end
35
+
36
+ it 'reads an unicode character' do
37
+ [*10.times.map { rand(160..0xD7FF).chr(Encoding::UTF_8) }.uniq,
38
+ # Range 0xD800..0xDFFF is used by UTF-16.
39
+ *10.times.map { rand(0xE000..0x10FFFF).chr(Encoding::UTF_8) }.uniq,
40
+ '가', 'あ', '漢', ' ', 'å', '★'].each do |chr|
41
+ allow($stdin).to receive(:getc).with(no_args).once
42
+ .and_return(chr)
43
+ expect(subject.read_chr).to be(chr.ord)
44
+ end
45
+ end
46
+ end
47
+
48
+ describe '.print_int' do
49
+ let(:value) { rand(-100..100) }
50
+
51
+ it { is_expected.to respond_to(:print_int) }
52
+
53
+ it 'prints an integer' do
54
+ allow($stdout).to receive(:print).with(value).once
55
+ expect(subject.print_int(value)).to be_nil
56
+ end
57
+ end
58
+
59
+ describe '.print_chr' do
60
+ it { is_expected.to respond_to(:print_chr) }
61
+
62
+ it 'prints an ASCII character corresponding character code' do
63
+ [0, *32..126].each do |value|
64
+ allow($stdout).to receive(:print).with(value.chr).once
65
+ expect(subject.print_chr(value)).to be_nil
66
+ end
67
+ end
68
+
69
+ it 'prints an control character corresponding character code' do
70
+ [*1..31, *127..159].each do |value|
71
+ allow($stdout).to receive(:print).with(value.chr(Encoding::UTF_8)).once
72
+ expect(subject.print_chr(value)).to be_nil
73
+ end
74
+ end
75
+
76
+ it 'prints an unicode character corresponding character code' do
77
+ [*10.times.map { rand(160..0xD7FF).chr(Encoding::UTF_8) }.uniq,
78
+ # Range 0xD800..0xDFFF is used by UTF-16.
79
+ *10.times.map { rand(0xE000..0x10FFFF).chr(Encoding::UTF_8) }.uniq,
80
+ '가', 'あ', '漢', ' ', 'å', '★'].each do |chr|
81
+ allow($stdout).to receive(:print).with(chr).once
82
+ expect(subject.print_chr(chr.ord)).to be_nil
83
+ end
84
+ end
85
+
86
+ it 'prints an [U+%04X] string when RangeError is raised' do
87
+ [*10.times.map { rand(0xD800..0xDFFF) }.uniq,
88
+ 0x110000].each do |value|
89
+ allow($stdout).to receive(:print).with(format('[U+%04X]', value)).once
90
+ expect(subject.print_chr(value)).to be_nil
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,43 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ describe Raheui::Option do
5
+ include ExitCodeMatchers
6
+
7
+ subject(:option) { described_class.new }
8
+
9
+ before(:example) { $stdout = StringIO.new }
10
+ after(:example) { $stdout = STDOUT }
11
+
12
+ describe 'option' do
13
+ describe '-h/--help' do
14
+ it 'exits cleanly' do
15
+ expect { option.parse(['-h']) }.to exit_with_code(0)
16
+ expect { option.parse(['--help']) }.to exit_with_code(0)
17
+ end
18
+
19
+ it 'shows help text' do
20
+ begin
21
+ option.parse(['--help'])
22
+ rescue SystemExit # rubocop:disable Lint/HandleExceptions
23
+ end
24
+
25
+ expected_help = <<-END
26
+ Usage: raheui [options] [file]
27
+ -h, --help Print this message.
28
+ -v, --version Print version.
29
+ END
30
+
31
+ expect($stdout.string).to eq(expected_help)
32
+ end
33
+ end
34
+
35
+ describe '-v/--version' do
36
+ it 'exits cleanly' do
37
+ expect { option.parse(['-v']) }.to exit_with_code(0)
38
+ expect { option.parse(['--version']) }.to exit_with_code(0)
39
+ expect($stdout.string).to eq("#{Raheui::Version::STRING}\n" * 2)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,6 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ describe Raheui::Port do
5
+ it_behaves_like 'a store'
6
+ end