Skip to content

Commit b6563cf

Browse files
authored
Add Buffered Reader (#3910)
1 parent 3e9dd77 commit b6563cf

File tree

2 files changed

+326
-0
lines changed

2 files changed

+326
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
package com.thealgorithms.io;
2+
3+
import java.io.ByteArrayInputStream;
4+
import java.io.IOException;
5+
import java.io.InputStream;
6+
7+
/**
8+
* Mimics the actions of the Original buffered reader
9+
* implements other actions, such as peek(n) to lookahead,
10+
* block() to read a chunk of size {BUFFER SIZE}
11+
* <p>
12+
* Author: Kumaraswamy B.G (Xoma Dev)
13+
*/
14+
public class BufferedReader {
15+
16+
private static final int DEFAULT_BUFFER_SIZE = 5;
17+
18+
/**
19+
* Maximum number of bytes the buffer can hold.
20+
* Value is changed when encountered Eof to not
21+
* cause overflow read of 0 bytes
22+
*/
23+
24+
private int bufferSize;
25+
private final byte[] buffer;
26+
27+
/**
28+
* posRead -> indicates the next byte to read
29+
*/
30+
private int posRead = 0, bufferPos = 0;
31+
32+
private boolean foundEof = false;
33+
34+
private InputStream input;
35+
36+
public BufferedReader(byte[] input) throws IOException {
37+
this(new ByteArrayInputStream(input));
38+
}
39+
40+
public BufferedReader(InputStream input) throws IOException {
41+
this(input, DEFAULT_BUFFER_SIZE);
42+
}
43+
44+
public BufferedReader(InputStream input, int bufferSize) throws IOException {
45+
this.input = input;
46+
if (input.available() == -1)
47+
throw new IOException("Empty or already closed stream provided");
48+
49+
this.bufferSize = bufferSize;
50+
buffer = new byte[bufferSize];
51+
}
52+
53+
/**
54+
* Reads a single byte from the stream
55+
*/
56+
public int read() throws IOException {
57+
if (needsRefill()) {
58+
if (foundEof)
59+
return -1;
60+
// the buffer is empty, or the buffer has
61+
// been completely read and needs to be refilled
62+
refill();
63+
}
64+
return buffer[posRead++] & 0xff; // read and un-sign it
65+
}
66+
67+
/**
68+
* Number of bytes not yet been read
69+
*/
70+
71+
public int available() throws IOException {
72+
int available = input.available();
73+
if (needsRefill())
74+
// since the block is already empty,
75+
// we have no responsibility yet
76+
return available;
77+
return bufferPos - posRead + available;
78+
}
79+
80+
/**
81+
* Returns the next character
82+
*/
83+
84+
public int peek() throws IOException {
85+
return peek(1);
86+
}
87+
88+
/**
89+
* Peeks and returns a value located at next {n}
90+
*/
91+
92+
public int peek(int n) throws IOException {
93+
int available = available();
94+
if (n >= available)
95+
throw new IOException("Out of range, available %d, but trying with %d"
96+
.formatted(available, n));
97+
pushRefreshData();
98+
99+
if (n >= bufferSize)
100+
throw new IllegalAccessError("Cannot peek %s, maximum upto %s (Buffer Limit)"
101+
.formatted(n, bufferSize));
102+
return buffer[n];
103+
}
104+
105+
/**
106+
* Removes the already read bytes from the buffer
107+
* in-order to make space for new bytes to be filled up.
108+
* <p>
109+
* This may also do the job to read first time data (whole buffer is empty)
110+
*/
111+
112+
private void pushRefreshData() throws IOException {
113+
for (int i = posRead, j = 0; i < bufferSize; i++, j++)
114+
buffer[j] = buffer[i];
115+
116+
bufferPos -= posRead;
117+
posRead = 0;
118+
119+
// fill out the spaces that we've
120+
// emptied
121+
justRefill();
122+
}
123+
124+
/**
125+
* Reads one complete block of size {bufferSize}
126+
* if found eof, the total length of array will
127+
* be that of what's available
128+
*
129+
* @return a completed block
130+
*/
131+
public byte[] readBlock() throws IOException {
132+
pushRefreshData();
133+
134+
byte[] cloned = new byte[bufferSize];
135+
// arraycopy() function is better than clone()
136+
if (bufferPos >= 0)
137+
System.arraycopy(buffer,
138+
0,
139+
cloned,
140+
0,
141+
// important to note that, bufferSize does not stay constant
142+
// once the class is defined. See justRefill() function
143+
bufferSize);
144+
// we assume that already a chunk
145+
// has been read
146+
refill();
147+
return cloned;
148+
}
149+
150+
private boolean needsRefill() {
151+
return bufferPos == 0 || posRead == bufferSize;
152+
}
153+
154+
private void refill() throws IOException {
155+
posRead = 0;
156+
bufferPos = 0;
157+
justRefill();
158+
}
159+
160+
private void justRefill() throws IOException {
161+
assertStreamOpen();
162+
163+
// try to fill in the maximum we can until
164+
// we reach EOF
165+
while (bufferPos < bufferSize) {
166+
int read = input.read();
167+
if (read == -1) {
168+
// reached end-of-file, no more data left
169+
// to be read
170+
foundEof = true;
171+
// rewrite the BUFFER_SIZE, to know that we've reached
172+
// EOF when requested refill
173+
bufferSize = bufferPos;
174+
}
175+
buffer[bufferPos++] = (byte) read;
176+
}
177+
}
178+
179+
private void assertStreamOpen() {
180+
if (input == null)
181+
throw new IllegalStateException("Input Stream already closed!");
182+
}
183+
184+
public void close() throws IOException {
185+
if (input != null) {
186+
try {
187+
input.close();
188+
} finally {
189+
input = null;
190+
}
191+
}
192+
}
193+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package com.thealgorithms.io;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import java.io.ByteArrayInputStream;
6+
import java.io.ByteArrayOutputStream;
7+
import java.io.IOException;
8+
import java.util.*;
9+
10+
import static org.junit.jupiter.api.Assertions.*;
11+
12+
class BufferedReaderTest {
13+
@Test
14+
public void testPeeks() throws IOException {
15+
String text = "Hello!\nWorld!";
16+
int len = text.length();
17+
byte[] bytes = text.getBytes();
18+
19+
ByteArrayInputStream input = new ByteArrayInputStream(bytes);
20+
BufferedReader reader = new BufferedReader(input);
21+
22+
// read the first letter
23+
assertEquals(reader.read(), 'H');
24+
len--;
25+
assertEquals(reader.available(), len);
26+
27+
// position: H[e]llo!\nWorld!
28+
// reader.read() will be == 'e'
29+
assertEquals(reader.peek(1), 'l');
30+
assertEquals(reader.peek(2), 'l'); // second l
31+
assertEquals(reader.peek(3), 'o');
32+
}
33+
34+
@Test
35+
public void testMixes() throws IOException {
36+
String text = "Hello!\nWorld!";
37+
int len = text.length();
38+
byte[] bytes = text.getBytes();
39+
40+
ByteArrayInputStream input = new ByteArrayInputStream(bytes);
41+
BufferedReader reader = new BufferedReader(input);
42+
43+
// read the first letter
44+
assertEquals(reader.read(), 'H'); // first letter
45+
len--;
46+
47+
assertEquals(reader.peek(1), 'l'); // third later (second letter after 'H')
48+
assertEquals(reader.read(), 'e'); // second letter
49+
len--;
50+
assertEquals(reader.available(), len);
51+
52+
// position: H[e]llo!\nWorld!
53+
assertEquals(reader.peek(2), 'o'); // second l
54+
assertEquals(reader.peek(3), '!');
55+
assertEquals(reader.peek(4), '\n');
56+
57+
assertEquals(reader.read(), 'l'); // third letter
58+
assertEquals(reader.peek(1), 'o'); // fourth letter
59+
60+
for (int i = 0; i < 6; i++)
61+
reader.read();
62+
try {
63+
System.out.println((char) reader.peek(4));
64+
} catch (Exception ignored) {
65+
System.out.println("[cached intentional error]");
66+
// intentional, for testing purpose
67+
}
68+
}
69+
70+
@Test
71+
public void testBlockPractical() throws IOException {
72+
String text = "!Hello\nWorld!";
73+
byte[] bytes = text.getBytes();
74+
int len = bytes.length;
75+
76+
ByteArrayInputStream input = new ByteArrayInputStream(bytes);
77+
BufferedReader reader = new BufferedReader(input);
78+
79+
80+
assertEquals(reader.peek(), 'H');
81+
assertEquals(reader.read(), '!'); // read the first letter
82+
len--;
83+
84+
// this only reads the next 5 bytes (Hello) because
85+
// the default buffer size = 5
86+
assertEquals(new String(reader.readBlock()), "Hello");
87+
len -= 5;
88+
assertEquals(reader.available(), len);
89+
90+
// maybe kind of a practical demonstration / use case
91+
if (reader.read() == '\n') {
92+
assertEquals(reader.read(), 'W');
93+
assertEquals(reader.read(), 'o');
94+
95+
// the rest of the blocks
96+
assertEquals(new String(reader.readBlock()), "rld!");
97+
} else {
98+
// should not reach
99+
throw new IOException("Something not right");
100+
}
101+
}
102+
103+
@Test
104+
public void randomTest() throws IOException {
105+
Random random = new Random();
106+
107+
int len = random.nextInt(9999);
108+
int bound = 256;
109+
110+
ByteArrayOutputStream stream = new ByteArrayOutputStream(len);
111+
while (len-- > 0)
112+
stream.write(random.nextInt(bound));
113+
114+
byte[] bytes = stream.toByteArray();
115+
ByteArrayInputStream comparer = new ByteArrayInputStream(bytes);
116+
117+
int blockSize = random.nextInt(7) + 5;
118+
BufferedReader reader = new BufferedReader(
119+
new ByteArrayInputStream(bytes), blockSize);
120+
121+
for (int i = 0; i < 50; i++) {
122+
if ((i & 1) == 0) {
123+
assertEquals(comparer.read(), reader.read());
124+
continue;
125+
}
126+
byte[] block = new byte[blockSize];
127+
comparer.read(block);
128+
byte[] read = reader.readBlock();
129+
130+
assertArrayEquals(block, read);
131+
}
132+
}
133+
}

0 commit comments

Comments
 (0)