Merge pull request #344 from shikorism/feature/319-csv-importer

CheckinCsvImporter
This commit is contained in:
shibafu 2020-05-18 01:05:58 +09:00 committed by GitHub
commit e872964144
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 800 additions and 3 deletions

View File

@ -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',

View File

@ -0,0 +1,29 @@
<?php
namespace App\Exceptions;
use Throwable;
class CsvImportException extends \RuntimeException
{
/** @var string[] */
private $errors;
/**
* CsvImportException constructor.
* @param string[] $errors
*/
public function __construct(...$errors)
{
parent::__construct(array_first($errors));
$this->errors = $errors;
}
/**
* @return string[]
*/
public function getErrors(): array
{
return $this->errors;
}
}

View File

@ -19,6 +19,8 @@ class AppServiceProvider extends ServiceProvider
Blade::directive('parsedown', function ($expression) {
return "<?php echo app('parsedown')->text($expression); ?>";
});
stream_filter_register('convert.mbstring.*', 'Stream_Filter_Mbstring');
}
/**

82
app/Rules/CsvDateTime.php Normal file
View File

@ -0,0 +1,82 @@
<?php
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
/**
* CSVインポート機能の日時バリデーションルール
* @package App\Rules
*/
class CsvDateTime implements Rule
{
const VALID_FORMATS = [
'Y/m/d H:i:s',
'Y/n/j G:i:s',
'Y/m/d H:i',
'Y/n/j G:i',
];
const MINIMUM_TIMESTAMP = 946652400; // 2000-01-01 00:00:00 JST
const MAXIMUM_TIMESTAMP = 4102412399; // 2099-12-31 23:59:59 JST
/** @var string Validation error message */
private $message = ':attributeの形式は "年/月/日 時:分" にしてください。';
/**
* Create a new rule instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Determine if the validation rule passes.
*
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value)
{
// この辺の実装の元ネタは、LaravelのValidatesAttributes#validateDateFormat()
if (!is_string($value)) {
return false;
}
foreach (self::VALID_FORMATS as $format) {
$date = \DateTime::createFromFormat('!' . $format, $value);
if (!$date) {
continue;
}
$timestamp = (int) $date->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;
}
}

View File

@ -0,0 +1,174 @@
<?php
namespace App\Services;
use App\Ejaculation;
use App\Exceptions\CsvImportException;
use App\Rules\CsvDateTime;
use App\Tag;
use App\User;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use League\Csv\Reader;
class CheckinCsvImporter
{
/** @var User Target user */
private $user;
/** @var string CSV filename */
private $filename;
public function __construct(User $user, string $filename)
{
$this->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;
}
}

View File

@ -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"

116
composer.lock generated
View File

@ -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": {

View File

@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddSourceToEjaculations extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('ejaculation_sources', function (Blueprint $table) {
$table->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');
}
}

View File

@ -11,6 +11,6 @@ class DatabaseSeeder extends Seeder
*/
public function run()
{
// $this->call(UsersTableSeeder::class);
$this->call(EjaculationSourcesSeeder::class);
}
}

View File

@ -0,0 +1,19 @@
<?php
use Illuminate\Database\Seeder;
class EjaculationSourcesSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
$sources = ['web', 'csv'];
foreach ($sources as $source) {
DB::table('ejaculation_sources')->insert(['name' => $source]);
}
}
}

View File

@ -0,0 +1,268 @@
<?php
namespace Tests\Unit\Services;
use App\Ejaculation;
use App\Exceptions\CsvImportException;
use App\Services\CheckinCsvImporter;
use App\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Carbon;
use Tests\TestCase;
class CheckinCsvImporterTest extends TestCase
{
use RefreshDatabase;
protected function setUp()
{
parent::setUp();
$this->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);
}
}

2
tests/fixture/Csv/.editorconfig vendored Normal file
View File

@ -0,0 +1,2 @@
[*.csv]
end_of_line = crlf

View File

@ -0,0 +1,2 @@
日時
2020/01/23 06:01
1 日時
2 2020/01/23 06:01

View File

@ -0,0 +1,2 @@
日時
2020/1/23 6:01
1 日時
2 2020/1/23 6:01

View File

@ -0,0 +1,2 @@
日時
2020/1/23 6:01:02
1 日時
2 2020/1/23 6:01:02

2
tests/fixture/Csv/date.utf8.csv vendored Normal file
View File

@ -0,0 +1,2 @@
日時
2020/01/23 06:01:02
1 日時
2 2020/01/23 06:01:02

View File

@ -0,0 +1,2 @@
日時,ノート,オカズリンク
2019/1/01 0:01,テストテストあああああ,https://example.com/
1 日時 ノート オカズリンク
2 2019/1/01 0:01 テストテストあああああ https://example.com/

17
tests/fixture/Csv/invalid-date.utf8.csv vendored Normal file
View File

@ -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,存在しない時刻
1 日時 ノート
2 1999/12/31 23:59:59 最小境界
3 2100/01/01 00:00:00 最大境界
4 -1/01/01 00:00:00 存在しない日付
5 2019/-1/01 00:00:00 存在しない日付
6 2019/01/-1 00:00:00 存在しない日付
7 2019/02/29 00:00:00 存在しない日付
8 2019/00/01 00:00:00 存在しない日付
9 2019/01/00 00:00:00 存在しない日付
10 2019/01/32 00:00:00 存在しない日付
11 2019/13/01 00:00:00 存在しない日付
12 2019/01/01 00:60:00 存在しない時刻
13 2019/01/01 24:00:00 存在しない時刻
14 2019/01/01 00:00:60 存在しない時刻
15 2019/01/01 -1:00:00 存在しない時刻
16 2019/01/01 00:-1:00 存在しない時刻
17 2019/01/01 00:00:-1 存在しない時刻

View File

@ -0,0 +1,3 @@
日時,オカズリンク
2020/01/23 06:01,example
1 日時 オカズリンク
2 2020/01/23 06:01 example

View File

@ -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
1 日時 オカズリンク
2 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
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/examp

2
tests/fixture/Csv/link.utf8.csv vendored Normal file
View File

@ -0,0 +1,2 @@
日時,オカズリンク
2020/01/23 06:01,http://example.com
1 日時 オカズリンク
2 2020/01/23 06:01 http://example.com

View File

@ -0,0 +1 @@
ノート,オカズリンク,タグ1
1 ノート オカズリンク タグ1

View File

@ -0,0 +1 @@
ノート,オカズリンク,タグ1
1 ノート オカズリンク タグ1

View File

@ -0,0 +1,2 @@
日時,ノート
2020/01/23 06:01,oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooox
1 日時 ノート
2 2020/01/23 06:01 oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooox

View File

@ -0,0 +1,2 @@
日時,ノート
2020/01/23 06:01,ああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああい
1 日時 ノート
2 2020/01/23 06:01 ああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああい

5
tests/fixture/Csv/note.utf8.csv vendored Normal file
View File

@ -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."
1 日時 ノート
2 2020/01/23 06:01 The quick brown fox jumps over the lazy dog. 素早い茶色の狐はのろまな犬を飛び越える
3 2020/01/23 06:02 The quick brown fox jumps over the lazy dog. 素早い茶色の狐はのろまな犬を飛び越える
4 2020/01/23 06:03 The quick brown fox jumps over the "lazy" dog.

View File

@ -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,あ,い,う,え,お,か,き,く,け,こ,さ,し,す,せ,そ,た,ち,つ,て,と,な,に,ぬ,ね,の,は,ひ,ふ,へ,ほ,ま,み,む
1 日時 タグ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
2 2020/01/23 06:01

View File

@ -0,0 +1,2 @@
日時,タグ1,タグ3
2020/01/23 06:01,貧乳,巨乳
1 日時 タグ1 タグ3
2 2020/01/23 06:01 貧乳 巨乳

View File

@ -0,0 +1,3 @@
日時,タグ1
2020/01/23 06:01,"複数行の
タグ"
1 日時 タグ1
2 2020/01/23 06:01 複数行の タグ

View File

@ -0,0 +1,3 @@
日時,タグ1
2020/01/23 06:01,ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
2020/01/23 06:02,ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooox
1 日時 タグ1
2 2020/01/23 06:01 ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
3 2020/01/23 06:02 ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooox

2
tests/fixture/Csv/tag1.utf8.csv vendored Normal file
View File

@ -0,0 +1,2 @@
日時,タグ1
2020/01/23 06:01,貧乳
1 日時 タグ1
2 2020/01/23 06:01 貧乳

2
tests/fixture/Csv/tag2.utf8.csv vendored Normal file
View File

@ -0,0 +1,2 @@
日時,タグ1,タグ2
2020/01/23 06:01,貧乳,巨乳
1 日時 タグ1 タグ2
2 2020/01/23 06:01 貧乳 巨乳