diff --git a/app/Ejaculation.php b/app/Ejaculation.php index 0f3522c..a270f40 100644 --- a/app/Ejaculation.php +++ b/app/Ejaculation.php @@ -12,6 +12,9 @@ class Ejaculation extends Model { use HasEagerLimit; + const SOURCE_WEB = 'web'; + const SOURCE_CSV = 'csv'; + protected $fillable = [ 'user_id', 'ejaculated_date', 'note', 'geo_latitude', 'geo_longitude', 'link', diff --git a/app/Exceptions/CsvImportException.php b/app/Exceptions/CsvImportException.php new file mode 100644 index 0000000..5571dc6 --- /dev/null +++ b/app/Exceptions/CsvImportException.php @@ -0,0 +1,29 @@ +errors = $errors; + } + + /** + * @return string[] + */ + public function getErrors(): array + { + return $this->errors; + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 675d30a..f952321 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -19,6 +19,8 @@ class AppServiceProvider extends ServiceProvider Blade::directive('parsedown', function ($expression) { return "text($expression); ?>"; }); + + stream_filter_register('convert.mbstring.*', 'Stream_Filter_Mbstring'); } /** diff --git a/app/Rules/CsvDateTime.php b/app/Rules/CsvDateTime.php new file mode 100644 index 0000000..02a5261 --- /dev/null +++ b/app/Rules/CsvDateTime.php @@ -0,0 +1,82 @@ +format('U'); + if ($timestamp < self::MINIMUM_TIMESTAMP || self::MAXIMUM_TIMESTAMP < $timestamp) { + $this->message = ':attributeは 2000/01/01 00:00 〜 2099/12/31 23:59 の間のみ対応しています。'; + + return false; + } + + $formatted = $date->format($format); + if ($formatted === $value) { + return true; + } + } + + return false; + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return $this->message; + } +} diff --git a/app/Services/CheckinCsvImporter.php b/app/Services/CheckinCsvImporter.php new file mode 100644 index 0000000..d99f51a --- /dev/null +++ b/app/Services/CheckinCsvImporter.php @@ -0,0 +1,174 @@ +user = $user; + $this->filename = $filename; + } + + public function execute() + { + // Guess charset + $charset = $this->guessCharset($this->filename); + + // Open CSV + $csv = Reader::createFromPath($this->filename, 'r'); + $csv->setHeaderOffset(0); + if ($charset === 'SJIS-win') { + $csv->addStreamFilter('convert.mbstring.encoding.SJIS-win:UTF-8'); + } + + // Import + DB::transaction(function () use ($csv) { + $errors = []; + + if (!in_array('日時', $csv->getHeader(), true)) { + $errors[] = '日時列は必須です。'; + } + + if (!empty($errors)) { + throw new CsvImportException(...$errors); + } + + foreach ($csv->getRecords() as $offset => $record) { + $line = $offset + 1; + $ejaculation = new Ejaculation(['user_id' => $this->user->id]); + + $validator = Validator::make($record, [ + '日時' => ['required', new CsvDateTime()], + 'ノート' => 'nullable|string|max:500', + 'オカズリンク' => 'nullable|url|max:2000', + ]); + + if ($validator->fails()) { + foreach ($validator->errors()->all() as $message) { + $errors[] = "{$line} 行 : {$message}"; + } + continue; + } + + $ejaculation->ejaculated_date = Carbon::createFromFormat('!Y/m/d H:i+', $record['日時']); + $ejaculation->note = str_replace(["\r\n", "\r"], "\n", $record['ノート'] ?? ''); + $ejaculation->link = $record['オカズリンク'] ?? ''; + $ejaculation->source = Ejaculation::SOURCE_CSV; + + try { + $tags = $this->parseTags($line, $record); + } catch (CsvImportException $e) { + $errors = array_merge($errors, $e->getErrors()); + continue; + } + + $ejaculation->save(); + if (!empty($tags)) { + $ejaculation->tags()->sync(collect($tags)->pluck('id')); + } + } + + if (!empty($errors)) { + throw new CsvImportException(...$errors); + } + }); + } + + /** + * 指定されたファイルを読み込み、文字コードの判定を行います。 + * @param string $filename CSVファイル名 + * @param int $samplingLength ファイルの先頭から何バイトを判定に使用するかを指定 + * @return string 検出した文字コード (UTF-8, SJIS-win, ...) + * @throws CsvImportException ファイルの読み込みに失敗した、文字コードを判定できなかった、または非対応文字コードを検出した場合にスロー + */ + private function guessCharset(string $filename, int $samplingLength = 1024): string + { + $fp = fopen($filename, 'rb'); + if (!$fp) { + throw new CsvImportException('CSVファイルの読み込み中にエラーが発生しました。'); + } + + try { + $head = fread($fp, $samplingLength); + if ($head === false) { + throw new CsvImportException('CSVファイルの読み込み中にエラーが発生しました。'); + } + + for ($addition = 0; $addition < 4; $addition++) { + $charset = mb_detect_encoding($head, ['ASCII', 'UTF-8', 'SJIS-win'], true); + if ($charset) { + if (array_search($charset, ['UTF-8', 'SJIS-win'], true) === false) { + throw new CsvImportException('文字コード判定に失敗しました。UTF-8 (BOM無し) または Shift_JIS をお使いください。'); + } else { + return $charset; + } + } + + // 1バイト追加で読み込んだら、文字境界に到達して上手く判定できるかもしれない + if (feof($fp)) { + break; + } + $next = fread($fp, 1); + if ($next === false) { + throw new CsvImportException('CSVファイルの読み込み中にエラーが発生しました。'); + } + $head .= $next; + } + + throw new CsvImportException('文字コード判定に失敗しました。UTF-8 (BOM無し) または Shift_JIS をお使いください。'); + } finally { + fclose($fp); + } + } + + /** + * タグ列をパースします。 + * @param int $line 現在の行番号 (1 origin) + * @param array $record 対象行のデータ + * @return Tag[] + * @throws CsvImportException バリデーションエラーが発生した場合にスロー + */ + private function parseTags(int $line, array $record): array + { + $tags = []; + foreach (array_keys($record) as $column) { + if (preg_match('/\Aタグ\d{1,2}\z/u', $column) !== 1) { + continue; + } + + $tag = trim($record[$column]); + if (empty($tag)) { + continue; + } + if (mb_strlen($tag) > 255) { + throw new CsvImportException("{$line} 行 : {$column}は255文字以内にしてください。"); + } + if (strpos($tag, "\n") !== false) { + throw new CsvImportException("{$line} 行 : {$column}に改行を含めることはできません。"); + } + + $tags[] = Tag::firstOrCreate(['name' => $tag]); + if (count($tags) >= 32) { + break; + } + } + + return $tags; + } +} diff --git a/composer.json b/composer.json index 3b89d39..9c5900a 100644 --- a/composer.json +++ b/composer.json @@ -4,6 +4,12 @@ "keywords": ["framework", "laravel"], "license": "MIT", "type": "project", + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/xcezx/Stream_Filter_Mbstring" + } + ], "require": { "php": ">=7.1.0", "anhskohbo/no-captcha": "^3.0", @@ -13,7 +19,9 @@ "jakeasmith/http_build_url": "^1.0", "laravel/framework": "5.5.*", "laravel/tinker": "~1.0", + "league/csv": "^9.5", "misd/linkify": "^1.1", + "openpear/stream_filter_mbstring": "dev-master", "staudenmeir/eloquent-eager-limit": "^1.0", "symfony/css-selector": "^4.3", "symfony/dom-crawler": "^4.3" diff --git a/composer.lock b/composer.lock index d21588d..e448a5d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b6dfb80c350a7276bb2513a1aeb3d602", + "content-hash": "a391908b8086044d943007dce1ff4f12", "packages": [ { "name": "anhskohbo/no-captcha", @@ -1190,6 +1190,75 @@ ], "time": "2019-08-07T15:10:45+00:00" }, + { + "name": "league/csv", + "version": "9.5.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/csv.git", + "reference": "b348d09d0d258a4f068efb50a2510dc63101c213" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/csv/zipball/b348d09d0d258a4f068efb50a2510dc63101c213", + "reference": "b348d09d0d258a4f068efb50a2510dc63101c213", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-mbstring": "*", + "php": ">=7.0.10" + }, + "require-dev": { + "ext-curl": "*", + "friendsofphp/php-cs-fixer": "^2.12", + "phpstan/phpstan": "^0.9.2", + "phpstan/phpstan-phpunit": "^0.9.4", + "phpstan/phpstan-strict-rules": "^0.9.0", + "phpunit/phpunit": "^6.0" + }, + "suggest": { + "ext-iconv": "Needed to ease transcoding CSV using iconv stream filters" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Csv\\": "src" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://github.com/nyamsprod/", + "role": "Developer" + } + ], + "description": "Csv data manipulation made easy in PHP", + "homepage": "http://csv.thephpleague.com", + "keywords": [ + "csv", + "export", + "filter", + "import", + "read", + "write" + ], + "time": "2019-12-15T19:51:41+00:00" + }, { "name": "league/flysystem", "version": "1.0.63", @@ -1555,6 +1624,47 @@ ], "time": "2019-11-08T13:50:10+00:00" }, + { + "name": "openpear/stream_filter_mbstring", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/xcezx/Stream_Filter_Mbstring.git", + "reference": "1c5ab27fd874e74d3d2bfdb9b74d3ebe017e6e14" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/xcezx/Stream_Filter_Mbstring/zipball/1c5ab27fd874e74d3d2bfdb9b74d3ebe017e6e14", + "reference": "1c5ab27fd874e74d3d2bfdb9b74d3ebe017e6e14", + "shasum": "" + }, + "type": "library", + "autoload": { + "psr-0": { + "Stream_Filter_Mbstring": "src/" + } + }, + "license": [ + "PHP-3.01" + ], + "authors": [ + { + "name": "hnw", + "role": "developer" + }, + { + "name": "MAEKAWA Tsuyoshi", + "email": "main.xcezx@gmail.com", + "role": "maintainer" + } + ], + "description": "mbstring を使って文字列変換を行う stream filter", + "support": { + "source": "https://github.com/xcezx/Stream_Filter_Mbstring/tree/master", + "issues": "https://github.com/xcezx/Stream_Filter_Mbstring/issues" + }, + "time": "2012-11-21T12:10:21+00:00" + }, { "name": "paragonie/random_compat", "version": "v9.99.99", @@ -6410,7 +6520,9 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": { + "openpear/stream_filter_mbstring": 20 + }, "prefer-stable": false, "prefer-lowest": false, "platform": { diff --git a/database/migrations/2020_02_17_085327_add_source_to_ejaculations.php b/database/migrations/2020_02_17_085327_add_source_to_ejaculations.php new file mode 100644 index 0000000..965e398 --- /dev/null +++ b/database/migrations/2020_02_17_085327_add_source_to_ejaculations.php @@ -0,0 +1,38 @@ +string('name'); + $table->primary('name'); + }); + Schema::table('ejaculations', function (Blueprint $table) { + $table->string('source')->nullable(); + $table->foreign('source')->references('name')->on('ejaculation_sources'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('ejaculations', function (Blueprint $table) { + $table->dropColumn('source'); + }); + Schema::drop('ejaculation_sources'); + } +} diff --git a/database/seeds/DatabaseSeeder.php b/database/seeds/DatabaseSeeder.php index e119db6..3f0fc32 100644 --- a/database/seeds/DatabaseSeeder.php +++ b/database/seeds/DatabaseSeeder.php @@ -11,6 +11,6 @@ class DatabaseSeeder extends Seeder */ public function run() { - // $this->call(UsersTableSeeder::class); + $this->call(EjaculationSourcesSeeder::class); } } diff --git a/database/seeds/EjaculationSourcesSeeder.php b/database/seeds/EjaculationSourcesSeeder.php new file mode 100644 index 0000000..a762a08 --- /dev/null +++ b/database/seeds/EjaculationSourcesSeeder.php @@ -0,0 +1,19 @@ +insert(['name' => $source]); + } + } +} diff --git a/tests/Unit/Services/CheckinCsvImporterTest.php b/tests/Unit/Services/CheckinCsvImporterTest.php new file mode 100644 index 0000000..51307da --- /dev/null +++ b/tests/Unit/Services/CheckinCsvImporterTest.php @@ -0,0 +1,268 @@ +seed(); + } + + public function testIncompatibleCharsetEUCJP() + { + $user = factory(User::class)->create(); + $this->expectException(CsvImportException::class); + $this->expectExceptionMessage('文字コード判定に失敗しました。UTF-8 (BOM無し) または Shift_JIS をお使いください。'); + + $importer = new CheckinCsvImporter($user, __DIR__ . '/../../fixture/Csv/incompatible-charset.eucjp.csv'); + $importer->execute(); + } + + /** + * @dataProvider provideMissingTime + */ + public function testMissingTime($filename) + { + $user = factory(User::class)->create(); + $this->expectException(CsvImportException::class); + $this->expectExceptionMessage('日時列は必須です。'); + + $importer = new CheckinCsvImporter($user, $filename); + $importer->execute(); + } + + public function provideMissingTime() + { + return [ + 'UTF8' => [__DIR__ . '/../../fixture/Csv/missing-time.utf8.csv'], + 'SJIS' => [__DIR__ . '/../../fixture/Csv/missing-time.sjis.csv'], + ]; + } + + /** + * @dataProvider provideDate + */ + public function testDate($expectedDate, $filename) + { + $user = factory(User::class)->create(); + + $importer = new CheckinCsvImporter($user, $filename); + $importer->execute(); + $ejaculation = $user->ejaculations()->first(); + + $this->assertSame(1, $user->ejaculations()->count()); + $this->assertEquals($expectedDate, $ejaculation->ejaculated_date); + } + + public function provideDate() + { + $date = Carbon::create(2020, 1, 23, 6, 1, 0, 'Asia/Tokyo'); + + return [ + 'Zero, Second, UTF8' => [$date, __DIR__ . '/../../fixture/Csv/date.utf8.csv'], + 'NoZero, Second, UTF8' => [$date, __DIR__ . '/../../fixture/Csv/date-nozero.utf8.csv'], + 'Zero, NoSecond, UTF8' => [$date, __DIR__ . '/../../fixture/Csv/date-nosecond.utf8.csv'], + 'NoZero, NoSecond, UTF8' => [$date, __DIR__ . '/../../fixture/Csv/date-nozero-nosecond.utf8.csv'], + ]; + } + + public function testInvalidDate() + { + $user = factory(User::class)->create(); + + try { + $importer = new CheckinCsvImporter($user, __DIR__ . '/../../fixture/Csv/invalid-date.utf8.csv'); + $importer->execute(); + } catch (CsvImportException $e) { + $this->assertSame('2 行 : 日時は 2000/01/01 00:00 〜 2099/12/31 23:59 の間のみ対応しています。', $e->getErrors()[0]); + $this->assertSame('3 行 : 日時は 2000/01/01 00:00 〜 2099/12/31 23:59 の間のみ対応しています。', $e->getErrors()[1]); + $this->assertSame('4 行 : 日時の形式は "年/月/日 時:分" にしてください。', $e->getErrors()[2]); + $this->assertSame('5 行 : 日時の形式は "年/月/日 時:分" にしてください。', $e->getErrors()[3]); + $this->assertSame('6 行 : 日時の形式は "年/月/日 時:分" にしてください。', $e->getErrors()[4]); + $this->assertSame('7 行 : 日時の形式は "年/月/日 時:分" にしてください。', $e->getErrors()[5]); + $this->assertSame('8 行 : 日時の形式は "年/月/日 時:分" にしてください。', $e->getErrors()[6]); + $this->assertSame('9 行 : 日時の形式は "年/月/日 時:分" にしてください。', $e->getErrors()[7]); + $this->assertSame('10 行 : 日時の形式は "年/月/日 時:分" にしてください。', $e->getErrors()[8]); + $this->assertSame('11 行 : 日時の形式は "年/月/日 時:分" にしてください。', $e->getErrors()[9]); + $this->assertSame('12 行 : 日時の形式は "年/月/日 時:分" にしてください。', $e->getErrors()[10]); + $this->assertSame('13 行 : 日時の形式は "年/月/日 時:分" にしてください。', $e->getErrors()[11]); + $this->assertSame('14 行 : 日時の形式は "年/月/日 時:分" にしてください。', $e->getErrors()[12]); + $this->assertSame('15 行 : 日時の形式は "年/月/日 時:分" にしてください。', $e->getErrors()[13]); + $this->assertSame('16 行 : 日時の形式は "年/月/日 時:分" にしてください。', $e->getErrors()[14]); + $this->assertSame('17 行 : 日時の形式は "年/月/日 時:分" にしてください。', $e->getErrors()[15]); + + return; + } + + $this->fail('期待する例外が発生していません'); + } + + public function testNoteUTF8() + { + $user = factory(User::class)->create(); + + $importer = new CheckinCsvImporter($user, __DIR__ . '/../../fixture/Csv/note.utf8.csv'); + $importer->execute(); + $ejaculations = $user->ejaculations()->orderBy('ejaculated_date')->get(); + + $this->assertCount(3, $ejaculations); + $this->assertEquals('The quick brown fox jumps over the lazy dog. 素早い茶色の狐はのろまな犬を飛び越える', $ejaculations[0]->note); + $this->assertEquals("The quick brown fox jumps over the lazy dog.\n素早い茶色の狐はのろまな犬を飛び越える", $ejaculations[1]->note); + $this->assertEquals('The quick brown fox jumps over the "lazy" dog.', $ejaculations[2]->note); + } + + /** + * @dataProvider provideNoteOverLength + */ + public function testNoteOverLength($filename) + { + $user = factory(User::class)->create(); + $this->expectException(CsvImportException::class); + $this->expectExceptionMessage('2 行 : ノートには500文字以下の文字列を指定してください。'); + + $importer = new CheckinCsvImporter($user, $filename); + $importer->execute(); + } + + public function provideNoteOverLength() + { + return [ + 'ASCII Only, UTF8' => [__DIR__ . '/../../fixture/Csv/note-over-length.ascii.utf8.csv'], + 'JP, UTF8' => [__DIR__ . '/../../fixture/Csv/note-over-length.jp.utf8.csv'], + ]; + } + + public function testLinkUTF8() + { + $user = factory(User::class)->create(); + + $importer = new CheckinCsvImporter($user, __DIR__ . '/../../fixture/Csv/link.utf8.csv'); + $importer->execute(); + $ejaculations = $user->ejaculations()->orderBy('ejaculated_date')->get(); + + $this->assertCount(1, $ejaculations); + $this->assertEquals('http://example.com', $ejaculations[0]->link); + } + + public function testLinkOverLengthUTF8() + { + $user = factory(User::class)->create(); + $this->expectException(CsvImportException::class); + $this->expectExceptionMessage('3 行 : オカズリンクには2000文字以下の文字列を指定してください。'); + + $importer = new CheckinCsvImporter($user, __DIR__ . '/../../fixture/Csv/link-over-length.utf8.csv'); + $importer->execute(); + } + + public function testLinkIsNotUrlUTF8() + { + $user = factory(User::class)->create(); + $this->expectException(CsvImportException::class); + $this->expectExceptionMessage('2 行 : オカズリンクには正しい形式のURLを指定してください。'); + + $importer = new CheckinCsvImporter($user, __DIR__ . '/../../fixture/Csv/link-not-url.utf8.csv'); + $importer->execute(); + } + + public function testTag1UTF8() + { + $user = factory(User::class)->create(); + + $importer = new CheckinCsvImporter($user, __DIR__ . '/../../fixture/Csv/tag1.utf8.csv'); + $importer->execute(); + $ejaculation = $user->ejaculations()->first(); + $tags = $ejaculation->tags()->get(); + + $this->assertSame(1, $user->ejaculations()->count()); + $this->assertCount(1, $tags); + $this->assertEquals('貧乳', $tags[0]->name); + } + + public function testTag2UTF8() + { + $user = factory(User::class)->create(); + + $importer = new CheckinCsvImporter($user, __DIR__ . '/../../fixture/Csv/tag2.utf8.csv'); + $importer->execute(); + $ejaculation = $user->ejaculations()->first(); + $tags = $ejaculation->tags()->get(); + + $this->assertSame(1, $user->ejaculations()->count()); + $this->assertCount(2, $tags); + $this->assertEquals('貧乳', $tags[0]->name); + $this->assertEquals('巨乳', $tags[1]->name); + } + + public function testTagOverLengthUTF8() + { + $user = factory(User::class)->create(); + $this->expectException(CsvImportException::class); + $this->expectExceptionMessage('3 行 : タグ1は255文字以内にしてください。'); + + $importer = new CheckinCsvImporter($user, __DIR__ . '/../../fixture/Csv/tag-over-length.utf8.csv'); + $importer->execute(); + } + + public function testTagCanAcceptJumpedColumnUTF8() + { + $user = factory(User::class)->create(); + + $importer = new CheckinCsvImporter($user, __DIR__ . '/../../fixture/Csv/tag-jumped-column.utf8.csv'); + $importer->execute(); + $ejaculation = $user->ejaculations()->first(); + $tags = $ejaculation->tags()->get(); + + $this->assertSame(1, $user->ejaculations()->count()); + $this->assertCount(2, $tags); + $this->assertEquals('貧乳', $tags[0]->name); + $this->assertEquals('巨乳', $tags[1]->name); + } + + public function testTagCantAcceptMultilineUTF8() + { + $user = factory(User::class)->create(); + $this->expectException(CsvImportException::class); + $this->expectExceptionMessage('2 行 : タグ1に改行を含めることはできません。'); + + $importer = new CheckinCsvImporter($user, __DIR__ . '/../../fixture/Csv/tag-multiline.utf8.csv'); + $importer->execute(); + } + + public function testTagCanAccept32ColumnsUTF8() + { + $user = factory(User::class)->create(); + + $importer = new CheckinCsvImporter($user, __DIR__ . '/../../fixture/Csv/tag-33-column.utf8.csv'); + $importer->execute(); + $ejaculation = $user->ejaculations()->first(); + $tags = $ejaculation->tags()->get(); + + $this->assertSame(1, $user->ejaculations()->count()); + $this->assertCount(32, $tags); + $this->assertEquals('み', $tags[31]->name); + } + + public function testSourceIsCsv() + { + $user = factory(User::class)->create(); + + $importer = new CheckinCsvImporter($user, __DIR__ . '/../../fixture/Csv/date.utf8.csv'); + $importer->execute(); + $ejaculation = $user->ejaculations()->first(); + + $this->assertSame(1, $user->ejaculations()->count()); + $this->assertEquals(Ejaculation::SOURCE_CSV, $ejaculation->source); + } +} diff --git a/tests/fixture/Csv/.editorconfig b/tests/fixture/Csv/.editorconfig new file mode 100644 index 0000000..c9e4d60 --- /dev/null +++ b/tests/fixture/Csv/.editorconfig @@ -0,0 +1,2 @@ +[*.csv] +end_of_line = crlf diff --git a/tests/fixture/Csv/date-nosecond.utf8.csv b/tests/fixture/Csv/date-nosecond.utf8.csv new file mode 100644 index 0000000..5285a20 --- /dev/null +++ b/tests/fixture/Csv/date-nosecond.utf8.csv @@ -0,0 +1,2 @@ +日時 +2020/01/23 06:01 diff --git a/tests/fixture/Csv/date-nozero-nosecond.utf8.csv b/tests/fixture/Csv/date-nozero-nosecond.utf8.csv new file mode 100644 index 0000000..436840f --- /dev/null +++ b/tests/fixture/Csv/date-nozero-nosecond.utf8.csv @@ -0,0 +1,2 @@ +日時 +2020/1/23 6:01 diff --git a/tests/fixture/Csv/date-nozero.utf8.csv b/tests/fixture/Csv/date-nozero.utf8.csv new file mode 100644 index 0000000..769cda9 --- /dev/null +++ b/tests/fixture/Csv/date-nozero.utf8.csv @@ -0,0 +1,2 @@ +日時 +2020/1/23 6:01:02 diff --git a/tests/fixture/Csv/date.utf8.csv b/tests/fixture/Csv/date.utf8.csv new file mode 100644 index 0000000..6e72631 --- /dev/null +++ b/tests/fixture/Csv/date.utf8.csv @@ -0,0 +1,2 @@ +日時 +2020/01/23 06:01:02 diff --git a/tests/fixture/Csv/incompatible-charset.eucjp.csv b/tests/fixture/Csv/incompatible-charset.eucjp.csv new file mode 100644 index 0000000..050a68d --- /dev/null +++ b/tests/fixture/Csv/incompatible-charset.eucjp.csv @@ -0,0 +1,2 @@ +,Ρ, +2019/1/01 0:01,ƥȥƥȤ,https://example.com/ diff --git a/tests/fixture/Csv/invalid-date.utf8.csv b/tests/fixture/Csv/invalid-date.utf8.csv new file mode 100644 index 0000000..2b542ba --- /dev/null +++ b/tests/fixture/Csv/invalid-date.utf8.csv @@ -0,0 +1,17 @@ +日時,ノート +1999/12/31 23:59:59,最小境界 +2100/01/01 00:00:00,最大境界 +-1/01/01 00:00:00,存在しない日付 +2019/-1/01 00:00:00,存在しない日付 +2019/01/-1 00:00:00,存在しない日付 +2019/02/29 00:00:00,存在しない日付 +2019/00/01 00:00:00,存在しない日付 +2019/01/00 00:00:00,存在しない日付 +2019/01/32 00:00:00,存在しない日付 +2019/13/01 00:00:00,存在しない日付 +2019/01/01 00:60:00,存在しない時刻 +2019/01/01 24:00:00,存在しない時刻 +2019/01/01 00:00:60,存在しない時刻 +2019/01/01 -1:00:00,存在しない時刻 +2019/01/01 00:-1:00,存在しない時刻 +2019/01/01 00:00:-1,存在しない時刻 diff --git a/tests/fixture/Csv/link-not-url.utf8.csv b/tests/fixture/Csv/link-not-url.utf8.csv new file mode 100644 index 0000000..9adb8c8 --- /dev/null +++ b/tests/fixture/Csv/link-not-url.utf8.csv @@ -0,0 +1,3 @@ +日時,オカズリンク +2020/01/23 06:01,example + diff --git a/tests/fixture/Csv/link-over-length.utf8.csv b/tests/fixture/Csv/link-over-length.utf8.csv new file mode 100644 index 0000000..45c33d0 --- /dev/null +++ b/tests/fixture/Csv/link-over-length.utf8.csv @@ -0,0 +1,3 @@ +日時,オカズリンク +2020/01/23 06:01,https://example.com/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/exam +2020/01/23 06:01,https://example.com/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/example/examp diff --git a/tests/fixture/Csv/link.utf8.csv b/tests/fixture/Csv/link.utf8.csv new file mode 100644 index 0000000..e7167fb --- /dev/null +++ b/tests/fixture/Csv/link.utf8.csv @@ -0,0 +1,2 @@ +日時,オカズリンク +2020/01/23 06:01,http://example.com diff --git a/tests/fixture/Csv/missing-time.sjis.csv b/tests/fixture/Csv/missing-time.sjis.csv new file mode 100644 index 0000000..8c88d46 --- /dev/null +++ b/tests/fixture/Csv/missing-time.sjis.csv @@ -0,0 +1 @@ +m[g,IJYN,^O1 diff --git a/tests/fixture/Csv/missing-time.utf8.csv b/tests/fixture/Csv/missing-time.utf8.csv new file mode 100644 index 0000000..b9176aa --- /dev/null +++ b/tests/fixture/Csv/missing-time.utf8.csv @@ -0,0 +1 @@ +ノート,オカズリンク,タグ1 diff --git a/tests/fixture/Csv/note-over-length.ascii.utf8.csv b/tests/fixture/Csv/note-over-length.ascii.utf8.csv new file mode 100644 index 0000000..02e0edf --- /dev/null +++ b/tests/fixture/Csv/note-over-length.ascii.utf8.csv @@ -0,0 +1,2 @@ +日時,ノート +2020/01/23 06:01,oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooox diff --git a/tests/fixture/Csv/note-over-length.jp.utf8.csv b/tests/fixture/Csv/note-over-length.jp.utf8.csv new file mode 100644 index 0000000..8daed63 --- /dev/null +++ b/tests/fixture/Csv/note-over-length.jp.utf8.csv @@ -0,0 +1,2 @@ +日時,ノート +2020/01/23 06:01,ああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああい diff --git a/tests/fixture/Csv/note.utf8.csv b/tests/fixture/Csv/note.utf8.csv new file mode 100644 index 0000000..872008f --- /dev/null +++ b/tests/fixture/Csv/note.utf8.csv @@ -0,0 +1,5 @@ +日時,ノート +2020/01/23 06:01,The quick brown fox jumps over the lazy dog. 素早い茶色の狐はのろまな犬を飛び越える +2020/01/23 06:02,"The quick brown fox jumps over the lazy dog. +素早い茶色の狐はのろまな犬を飛び越える" +2020/01/23 06:03,"The quick brown fox jumps over the ""lazy"" dog." diff --git a/tests/fixture/Csv/tag-33-column.utf8.csv b/tests/fixture/Csv/tag-33-column.utf8.csv new file mode 100644 index 0000000..85670a2 --- /dev/null +++ b/tests/fixture/Csv/tag-33-column.utf8.csv @@ -0,0 +1,2 @@ +日時,タグ1,タグ2,タグ3,タグ4,タグ5,タグ6,タグ7,タグ8,タグ9,タグ10,タグ11,タグ12,タグ13,タグ14,タグ15,タグ16,タグ17,タグ18,タグ19,タグ20,タグ21,タグ22,タグ23,タグ24,タグ25,タグ26,タグ27,タグ28,タグ29,タグ30,タグ31,タグ32,タグ33 +2020/01/23 06:01,あ,い,う,え,お,か,き,く,け,こ,さ,し,す,せ,そ,た,ち,つ,て,と,な,に,ぬ,ね,の,は,ひ,ふ,へ,ほ,ま,み,む diff --git a/tests/fixture/Csv/tag-jumped-column.utf8.csv b/tests/fixture/Csv/tag-jumped-column.utf8.csv new file mode 100644 index 0000000..773db36 --- /dev/null +++ b/tests/fixture/Csv/tag-jumped-column.utf8.csv @@ -0,0 +1,2 @@ +日時,タグ1,タグ3 +2020/01/23 06:01,貧乳,巨乳 diff --git a/tests/fixture/Csv/tag-multiline.utf8.csv b/tests/fixture/Csv/tag-multiline.utf8.csv new file mode 100644 index 0000000..ee1bd38 --- /dev/null +++ b/tests/fixture/Csv/tag-multiline.utf8.csv @@ -0,0 +1,3 @@ +日時,タグ1 +2020/01/23 06:01,"複数行の +タグ" diff --git a/tests/fixture/Csv/tag-over-length.utf8.csv b/tests/fixture/Csv/tag-over-length.utf8.csv new file mode 100644 index 0000000..891308e --- /dev/null +++ b/tests/fixture/Csv/tag-over-length.utf8.csv @@ -0,0 +1,3 @@ +日時,タグ1 +2020/01/23 06:01,ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo +2020/01/23 06:02,ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooox diff --git a/tests/fixture/Csv/tag1.utf8.csv b/tests/fixture/Csv/tag1.utf8.csv new file mode 100644 index 0000000..1d7561c --- /dev/null +++ b/tests/fixture/Csv/tag1.utf8.csv @@ -0,0 +1,2 @@ +日時,タグ1 +2020/01/23 06:01,貧乳 diff --git a/tests/fixture/Csv/tag2.utf8.csv b/tests/fixture/Csv/tag2.utf8.csv new file mode 100644 index 0000000..10e82fd --- /dev/null +++ b/tests/fixture/Csv/tag2.utf8.csv @@ -0,0 +1,2 @@ +日時,タグ1,タグ2 +2020/01/23 06:01,貧乳,巨乳