diff --git a/app/Exceptions/CsvImportException.php b/app/Exceptions/CsvImportException.php new file mode 100644 index 0000000..eee24cf --- /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/Io/CheckinCsvImporter.php b/app/Io/CheckinCsvImporter.php new file mode 100644 index 0000000..b02d309 --- /dev/null +++ b/app/Io/CheckinCsvImporter.php @@ -0,0 +1,76 @@ +filename = $filename; + } + + public function execute() + { + // Guess charset + $head = file_get_contents($this->filename, false, null, 0, 1024); + $charset = mb_detect_encoding($head, ['ASCII', 'UTF-8', 'SJIS-win'], true); + if (array_search($charset, ['UTF-8', 'SJIS-win'], true) === false) { + throw new CsvImportException(['文字コード判定に失敗しました。UTF-8 (BOM無し) または Shift_JIS をお使いください。']); + } + + // 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[] = '日時列は必須です。'; + throw new CsvImportException($errors); + } + + foreach ($csv->getRecords() as $offset => $record) { + $ejaculation = new Ejaculation(); + + $checkinAt = $record['日時'] ?? null; + if (empty($checkinAt)) { + $errors[] = "{$offset} 行 : 日時列は必須です。"; + continue; + } + if (preg_match('/\A20\d{2}[-/](1[0-2]|0?\d)[-/](0?\d|[1-2]\d|3[01]) (0?\d|1\d|2[0-4]):(0?\d|[1-5]\d)(?P:(0?\d|[1-5]\d))?\z/', $checkinAt, $checkinAtMatches) !== 1) { + $errors[] = "{$offset} 行 : 日時列の書式が正しくありません。"; + continue; + } + if (empty($checkinAtMatches['second'])) { + $checkinAt .= ':00'; + } + $checkinAt = str_replace('/', '-', $checkinAt); + try { + $ejaculation->ejaculated_date = Carbon::createFromFormat('Y-m-d H:i:s', $checkinAt); + } catch (\InvalidArgumentException $e) { + $errors[] = "{$offset} 行 : 日時列に不正な値が入力されています。"; + } + + $ejaculation->save(); + } + + if (!empty($errors)) { + throw new CsvImportException($errors); + } + }); + } +} diff --git a/tests/Unit/Io/CheckinCsvImporterTest.php b/tests/Unit/Io/CheckinCsvImporterTest.php new file mode 100644 index 0000000..a85935d --- /dev/null +++ b/tests/Unit/Io/CheckinCsvImporterTest.php @@ -0,0 +1,37 @@ +expectException(CsvImportException::class); + $this->expectExceptionMessage('文字コード判定に失敗しました。UTF-8 (BOM無し) または Shift_JIS をお使いください。'); + + $importer = new CheckinCsvImporter(__DIR__ . '/../../fixture/Csv/incompatible-charset.eucjp.csv'); + $importer->execute(); + } + + public function testMissingTimeUTF8() + { + $this->expectException(CsvImportException::class); + $this->expectExceptionMessage('日時列は必須です。'); + + $importer = new CheckinCsvImporter(__DIR__ . '/../../fixture/Csv/missing-time.utf8.csv'); + $importer->execute(); + } + + public function testMissingTimeSJIS() + { + $this->expectException(CsvImportException::class); + $this->expectExceptionMessage('日時列は必須です。'); + + $importer = new CheckinCsvImporter(__DIR__ . '/../../fixture/Csv/missing-time.sjis.csv'); + $importer->execute(); + } +} diff --git a/tests/fixture/Csv/incompatible-charset.eucjp.csv b/tests/fixture/Csv/incompatible-charset.eucjp.csv new file mode 100644 index 0000000..866ed4d --- /dev/null +++ b/tests/fixture/Csv/incompatible-charset.eucjp.csv @@ -0,0 +1,2 @@ +,Ρ, +2019-01-01 00:01:02,ƥȥƥȤ,https://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..99b9d73 --- /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..b0bc641 --- /dev/null +++ b/tests/fixture/Csv/missing-time.utf8.csv @@ -0,0 +1 @@ +ノート,オカズリンク,タグ1