Skip to content

Commit 31762cf

Browse files
authored
Add Char::Reader#current_char?, #next_char?, #previous_char? (#14012)
1 parent f3b47fb commit 31762cf

File tree

2 files changed

+142
-26
lines changed

2 files changed

+142
-26
lines changed

spec/std/char/reader_spec.cr

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,55 @@ describe "Char::Reader" do
145145
reader.current_char.should eq('語')
146146
end
147147

148+
it "#current_char?" do
149+
reader = Char::Reader.new("há日本語")
150+
reader.current_char?.should eq('h')
151+
reader.next_char
152+
reader.current_char?.should eq('á')
153+
reader.next_char
154+
reader.current_char?.should eq('日')
155+
reader.next_char
156+
reader.current_char?.should eq('本')
157+
reader.next_char
158+
reader.current_char?.should eq('語')
159+
reader.next_char
160+
reader.current_char?.should be_nil
161+
reader.previous_char
162+
reader.current_char?.should eq('語')
163+
end
164+
165+
it "#next_char?" do
166+
reader = Char::Reader.new("há日本語")
167+
reader.next_char?.should eq('á')
168+
reader.pos.should eq(1)
169+
reader.next_char?.should eq('日')
170+
reader.pos.should eq(3)
171+
reader.next_char?.should eq('本')
172+
reader.pos.should eq(6)
173+
reader.next_char?.should eq('語')
174+
reader.pos.should eq(9)
175+
reader.next_char?.should be_nil
176+
reader.pos.should eq(12)
177+
reader.next_char?.should be_nil
178+
reader.pos.should eq(12)
179+
end
180+
181+
it "#previous_char?" do
182+
reader = Char::Reader.new("há日本語", pos: 12)
183+
reader.previous_char?.should eq('語')
184+
reader.pos.should eq(9)
185+
reader.previous_char?.should eq('本')
186+
reader.pos.should eq(6)
187+
reader.previous_char?.should eq('日')
188+
reader.pos.should eq(3)
189+
reader.previous_char?.should eq('á')
190+
reader.pos.should eq(1)
191+
reader.previous_char?.should eq('h')
192+
reader.pos.should eq(0)
193+
reader.previous_char?.should be_nil
194+
reader.pos.should eq(0)
195+
end
196+
148197
it "errors if 0x80 <= first_byte < 0xC2" do
149198
assert_invalid_byte_sequence Bytes[0x80]
150199
assert_invalid_byte_sequence Bytes[0xC1]

src/char/reader.cr

Lines changed: 93 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,16 @@ struct Char
3939
# Returns the reader's String.
4040
getter string : String
4141

42-
# Returns the current character.
42+
# Returns the current character, or `'\0'` if the reader is at the end of
43+
# the string.
4344
#
4445
# ```
4546
# reader = Char::Reader.new("ab")
4647
# reader.current_char # => 'a'
4748
# reader.next_char
4849
# reader.current_char # => 'b'
50+
# reader.next_char
51+
# reader.current_char # => '\0'
4952
# ```
5053
getter current_char : Char
5154

@@ -59,7 +62,7 @@ struct Char
5962
# ```
6063
getter current_char_width : Int32
6164

62-
# Returns the position of the current character.
65+
# Returns the byte position of the current character.
6366
#
6467
# ```
6568
# reader = Char::Reader.new("ab")
@@ -93,40 +96,81 @@ struct Char
9396
decode_previous_char
9497
end
9598

96-
# Returns `true` if there is a character left to read.
97-
# The terminating byte `'\0'` is considered a valid character
98-
# by this method.
99+
# Returns the current character.
100+
#
101+
# Returns `nil` if the reader is at the end of the string.
102+
def current_char? : Char?
103+
if has_next?
104+
current_char
105+
end
106+
end
107+
108+
# Returns `true` if the reader is not at the end of the string.
109+
#
110+
# NOTE: This only means `#next_char` will successfully increment `#pos`; if
111+
# the reader is already at the last character, `#next_char` will return the
112+
# terminating null byte because there isn't really a next character.
99113
#
100114
# ```
101-
# reader = Char::Reader.new("a")
102-
# reader.has_next? # => true
103-
# reader.peek_next_char # => '\0'
115+
# reader = Char::Reader.new("ab")
116+
# reader.has_next? # => true
117+
# reader.next_char # => 'b'
118+
# reader.has_next? # => true
119+
# reader.next_char # => '\0'
120+
# reader.has_next? # => false
104121
# ```
105122
def has_next? : Bool
106123
@pos < @string.bytesize
107124
end
108125

109-
# Reads the next character in the string,
110-
# `#pos` is incremented. Raises `IndexError` if the reader is
111-
# at the end of the `#string`.
126+
# Tries to read the next character in the string.
127+
#
128+
# If the reader is at the end of the string before or after incrementing
129+
# `#pos`, returns `nil`.
112130
#
113131
# ```
114-
# reader = Char::Reader.new("ab")
132+
# reader = Char::Reader.new("abc")
133+
# reader.next_char? # => 'b'
134+
# reader.next_char? # => 'c'
135+
# reader.next_char? # => nil
136+
# reader.current_char # => '\0'
137+
# ```
138+
def next_char? : Char?
139+
next_pos = @pos + @current_char_width
140+
if next_pos <= @string.bytesize
141+
@pos = next_pos
142+
decode_current_char
143+
current_char?
144+
end
145+
end
146+
147+
# Reads the next character in the string.
148+
#
149+
# If the reader is at the end of the string after incrementing `#pos`,
150+
# returns `'\0'`. If the reader is already at the end beforehand, raises
151+
# `IndexError`.
152+
#
153+
# ```
154+
# reader = Char::Reader.new("abc")
115155
# reader.next_char # => 'b'
156+
# reader.next_char # => 'c'
157+
# reader.next_char # => '\0'
158+
# reader.next_char # raise IndexError
116159
# ```
117160
def next_char : Char
118-
@pos += @current_char_width
119-
if @pos > @string.bytesize
161+
next_pos = @pos + @current_char_width
162+
if next_pos <= @string.bytesize
163+
@pos = next_pos
164+
decode_current_char
165+
else
120166
raise IndexError.new
121167
end
122-
123-
decode_current_char
124168
end
125169

126-
# Returns the next character in the `#string`
127-
# without incrementing `#pos`.
128-
# Raises `IndexError` if the reader is at
129-
# the end of the `#string`.
170+
# Returns the next character in the `#string` without incrementing `#pos`.
171+
#
172+
# Returns `'\0'` if the reader is at the last character of the string.
173+
# Raises `IndexError` if the reader is at the end.
130174
#
131175
# ```
132176
# reader = Char::Reader.new("ab")
@@ -145,16 +189,39 @@ struct Char
145189
end
146190
end
147191

148-
# Returns `true` if there are characters before
149-
# the current one.
192+
# Returns `true` if the reader is not at the beginning of the string.
150193
def has_previous? : Bool
151194
@pos > 0
152195
end
153196

154-
# Returns the previous character, `#pos`
155-
# is decremented.
156-
# Raises `IndexError` if the reader is at the beginning of
157-
# the `#string`
197+
# Tries to read the previous character in the string.
198+
#
199+
# Returns `nil` if the reader is already at the beginning of the string.
200+
# Otherwise decrements `#pos`.
201+
#
202+
# ```
203+
# reader = Char::Reader.new(at_end: "abc")
204+
# reader.previous_char? # => 'b'
205+
# reader.previous_char? # => 'a'
206+
# reader.previous_char? # => nil
207+
# ```
208+
def previous_char? : Char?
209+
if has_previous?
210+
decode_previous_char
211+
end
212+
end
213+
214+
# Reads the previous character in the string.
215+
#
216+
# Raises `IndexError` if the reader is already at the beginning of the
217+
# string. Otherwise decrements `#pos`.
218+
#
219+
# ```
220+
# reader = Char::Reader.new(at_end: "abc")
221+
# reader.previous_char # => 'b'
222+
# reader.previous_char # => 'a'
223+
# reader.previous_char # raises IndexError
224+
# ```
158225
def previous_char : Char
159226
unless has_previous?
160227
raise IndexError.new

0 commit comments

Comments
 (0)