ファイルから1行ずつ文字列を取得するクラスを自作してみた


ファイルから文字列を読み込むには、NSStringを初期化するときにファイルパスを指定する方法が使えます。
そして、文字列を改行コードで区切って1行ずつ取得する機能はNSStringクラスに備わっているので、次のようなコードで簡単に実装できます。

```objc:
NSString *path = @"/path/for/file.txt";
NSString *str = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
// 1行ずつ文字列を列挙
[str enumerateLinesUsingBlock:^(NSString *line, BOOL *stop) {
    NSLog(@"%@", line);
}];
```
大抵の場合はこの方法で良いのですが、NSStringのインスタンスを作るときにファイルの内容を全てメモリ上に読み込むので、巨大なファイルを扱おうとすると、メモリ消費量が大きくなります。
.NETならStreamReaderを使えばファイルのデータを少しずつメモリ上のバッファに読み取って行を列挙できますが、Cocoaでそのようなクラスを見つけることはできませんでした。

ちょっと大きめのファイルから1行ずつ文字列を取得する必要が出てきたので、NSStringを介さずに直接ファイルから読み出すクラスを自作してみました。
同じコードが[GitHub](https://github.com/yaslab/YLFileReader)にも置いてあります。

まずは使い方から。

```objc:
NSString *path = @"/path/for/file.txt";
YLFileReader *reader = [[YLFileReader alloc] initWithFilePath:path encoding:NSUTF8StringEncoding];
// 1行ずつ文字列を列挙
NSString *line;
while ((line = [reader readLine]) != nil) {
    NSLog(@"%@", line);
}
[reader close];
```

次に自作したクラスです。
簡単にしかテストしていませんが、ASCII、UTF-8、Shift-JIS、EUC-JP、UTF-16(LE,BE)、UTF-32(LE,BE)のテキストファイルが読めることを確認済みです。改行コードはCR、LF、CRLFに対応しています。
また、初期化時にメモリ上のバッファ領域のサイズを指定できます。デフォルト値は128バイトですが、この値を大きくすると高速になります。バッファサイズが4の倍数になるように調整しているのは、UTF-16やUTF-32の文字列を読むときに処理が複雑になるのを避けるためです。

```objc:YLFileReader.h
#import <Foundation/Foundation.h>

@interface YLFileReader : NSObject

- (id)initWithFilePath:(NSString *)path;
- (id)initWithFilePath:(NSString *)path encoding:(NSStringEncoding)encoding;
- (id)initWithFilePath:(NSString *)path encoding:(NSStringEncoding)encoding bufferSize:(NSInteger)size;

- (NSString *)readLine;

- (void)close;

@end
```

```objc:YLFileReader.m
#import "YLFileReader.h"

#define kDefaultBufferSize 128

#define kLineBreakCodeLF 0x0a //'\n'
#define kLineBreakCodeCR 0x0d //'\r'

@interface YLFileReader () {
    FILE *_stream;
    NSStringEncoding _encoding;
    NSInteger _bufferSize;
    uint8_t *_buffer;
    NSInteger _offset;
    NSInteger _width;
    size_t _readCount;
    BOOL _isReadEnd;
    NSMutableData *_data;
}
@end

@implementation YLFileReader

- (id)initWithFilePath:(NSString *)path
{
    return [self initWithFilePath:path encoding:NSUTF8StringEncoding bufferSize:kDefaultBufferSize];
}

- (id)initWithFilePath:(NSString *)path encoding:(NSStringEncoding)encoding
{
    return [self initWithFilePath:path encoding:encoding bufferSize:kDefaultBufferSize];
}

- (id)initWithFilePath:(NSString *)path encoding:(NSStringEncoding)encoding bufferSize:(NSInteger)size
{
    self = [super init];
    if (self) {
        if (size <= 0) {
            return nil;
        }
        else if (size < 8) {
            size = 8;
        }
        _stream = fopen(path.UTF8String, "r");
        if (_stream == NULL) {
            return nil;
        }
        _encoding = encoding;
        _bufferSize = size - (size % 4);
        _buffer = (uint8_t *)malloc(sizeof(uint8_t) * _bufferSize);
        if (_buffer == NULL) {
            fclose(_stream);
            return nil;
        }
        _offset = 0;
        _width = sizeof(uint8_t);
        _readCount = 0;
        _isReadEnd = NO;
        _data = [NSMutableData new];

        // check Unicode BOM and set width
        switch (encoding) {
            case NSUTF16StringEncoding: { //==NSUnicodeStringEncoding
                uint8_t bom[2];
                size_t count = fread(bom, sizeof(uint8_t), 2, _stream);
                if (ferror(_stream)) {
                    [self close];
                    return nil;
                }
                if (count == 2) {
                    if (bom[0] == 0xfe && bom[1] == 0xff) {
                        _encoding = NSUTF16BigEndianStringEncoding;
                    }
                    else if (bom[0] == 0xff && bom[1] == 0xfe) {
                        _encoding = NSUTF16LittleEndianStringEncoding;
                    }
                    else {
                        _encoding = NSUTF16BigEndianStringEncoding;
                        fseek(_stream, 0L, SEEK_SET);
                    }
                }
                else {
                    _encoding = NSUTF16BigEndianStringEncoding;
                    fseek(_stream, 0L, SEEK_SET);
                }
            }
            case NSUTF16BigEndianStringEncoding:
            case NSUTF16LittleEndianStringEncoding: {
                _width = sizeof(uint16_t);
                break;
            }
            case NSUTF32StringEncoding: {
                uint8_t bom[4];
                size_t count = fread(bom, sizeof(uint8_t), 4, _stream);
                if (ferror(_stream)) {
                    [self close];
                    return nil;
                }
                if (count == 4) {
                    if (bom[0] == 0x00 && bom[1] == 0x00 && bom[2] == 0xfe && bom[3] == 0xff) {
                        _encoding = NSUTF32BigEndianStringEncoding;
                    }
                    else if (bom[0] == 0xff && bom[1] == 0xfe && bom[2] == 0x00 && bom[3] == 0x00) {
                        _encoding = NSUTF32LittleEndianStringEncoding;
                    }
                    else {
                        _encoding = NSUTF32BigEndianStringEncoding;
                        fseek(_stream, 0L, SEEK_SET);
                    }
                }
                else {
                    _encoding = NSUTF32BigEndianStringEncoding;
                    fseek(_stream, 0L, SEEK_SET);
                }
            }
            case NSUTF32BigEndianStringEncoding:
            case NSUTF32LittleEndianStringEncoding: {
                _width = sizeof(uint32_t);
                break;
            }
            default:
                break;
        }
    }
    return self;
}

- (void)dealloc
{
    [self close];
}

- (void)close
{
    if (_stream != NULL) {
        fclose(_stream);
        _stream = NULL;
    }
    if (_buffer != NULL) {
        free(_buffer);
        _buffer = NULL;
    }
    _isReadEnd = YES;
}

- (NSString *)readLine
{
    if (_isReadEnd) {
        return nil;
    }
    _data.length = 0;
    NSInteger start = _offset;
    NSInteger length = 0;
    BOOL hasCR = NO;
    while (YES) {
        if (_offset >= _readCount) {
            if (length > 0) {
                [_data appendBytes:(_buffer + start) length:(_width * length)];
            }
            _offset = 0;
            start = 0;
            length = 0;
            _readCount = fread(_buffer, sizeof(uint8_t), _bufferSize, _stream);
            if (_readCount == 0) {
                _isReadEnd = YES;
                if (_data.length == 0) { return nil; }
                else { break; }
            }
        }
        
        uint8_t c = 0x00;
        if (_width == sizeof(uint8_t)) {
            c = *(_buffer + _offset);
        }
        else if (_width == sizeof(uint16_t)) {
            uint8_t c0, c1;
            c0 = *(_buffer + _offset);
            c1 = *(_buffer + _offset + 1);
            if (_encoding == NSUTF16LittleEndianStringEncoding) {
                if (c0 == kLineBreakCodeLF && c1 == 0x00) { c = kLineBreakCodeLF; }
                else if (c0 == kLineBreakCodeCR && c1 == 0x00) { c = kLineBreakCodeCR; }
            }
            else {
                if (c0 == 0x00 && c1 == kLineBreakCodeLF) { c = kLineBreakCodeLF; }
                else if (c0 == 0x00 && c1 == kLineBreakCodeCR) { c = kLineBreakCodeCR; }
            }
        }
        else if (_width == sizeof(uint32_t)) {
            uint8_t c0, c1, c2, c3;
            c0 = *(_buffer + _offset);
            c1 = *(_buffer + _offset + 1);
            c2 = *(_buffer + _offset + 2);
            c3 = *(_buffer + _offset + 3);
            if (_encoding == NSUTF32LittleEndianStringEncoding) {
                if (c0 == kLineBreakCodeLF && c1 == 0x00 && c2 == 0x00 && c3 == 0x00) { c = kLineBreakCodeLF; }
                else if (c0 == kLineBreakCodeCR && c1 == 0x00 && c2 == 0x00 && c3 == 0x00) { c = kLineBreakCodeCR; }
            }
            else {
                if (c0 == 0x00 && c1 == 0x00 && c2 == 0x00 && c3 == kLineBreakCodeLF) { c = kLineBreakCodeLF; }
                else if (c0 == 0x00 && c1 == 0x00 && c2 == 0x00 && c3 == kLineBreakCodeCR) { c = kLineBreakCodeCR; }
            }
        }

        if (c == kLineBreakCodeLF) {
            // 1行取得完了('\n' or '\r''\n')
            _offset += _width;
            break;
        }
        else if (hasCR) {
            // 1つ前が'\r'で、今回が'\n'でない場合、オフセットは加算しない
            // 1行取得完了('\r')
            break;
        }
        if (c == kLineBreakCodeCR) {
            // 次に'\n'が来るかもしれないので、フラグを立てておく
            hasCR = YES;
            // 次が'\n'かどうかを確認するためにコンティニュー
            _offset += _width;
            continue;
        }

        _offset += _width;
        length++;
    }
    if (length > 0) {
        [_data appendBytes:(_buffer + start) length:(_width * length)];
    }
    
    NSString *line = [[NSString alloc] initWithData:_data encoding:_encoding];
    if (line == nil) {
        ; //Error occured
    }
    return line;
}

@end
```