diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..87ba2b3 --- /dev/null +++ b/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "ff37bef603469fb030f2b72995ab929ccfc227f0" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0 + base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0 + - platform: web + create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0 + base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/lib/main_view.dart b/lib/main_view.dart index 7298a1c..c3d45d9 100644 --- a/lib/main_view.dart +++ b/lib/main_view.dart @@ -262,11 +262,18 @@ class _MainState extends State { } String? _validateNgayThangNam(String? value) { - if (value!.isEmpty) return 'Cần nhập ngày âm'; - final nameExp = RegExp( - r'^(?:31([/\-.])(?:0?[13578]|1[02])\1|(?:29|30)(/)(?:0?[13-9]|1[0-2])\2)(?:1[6-9]|[2-9]\d)?\d{2}$|^29([/\-.])0?2\3(?:(?:1[6-9]|[2-9]\d)?(?:0[48]|[2468][048]|[13579][26])|(?:16|[2468][048]|[3579][26])00)$|^(?:0?[1-9]|1\d|2[0-8])([/\-.])(?:0?[1-9]|1[0-2])\4(?:1[6-9]|[2-9]\d)?\d{2}$', - ); - if (!nameExp.hasMatch(value)) return 'Nhập theo Ngày/Tháng/Năm'; + if (value == null || value.isEmpty) return 'Cần nhập ngày âm'; + final parts = value.split('/'); + if (parts.length != 3) return 'Nhập theo Ngày/Tháng/Năm'; + final day = int.tryParse(parts[0]); + final month = int.tryParse(parts[1]); + final year = int.tryParse(parts[2]); + if (day == null || month == null || year == null) { + return 'Nhập theo Ngày/Tháng/Năm'; + } + if (day < 1 || day > 30) return 'Ngày từ 1 đến 30'; + if (month < 1 || month > 12) return 'Tháng từ 1 đến 12'; + if (year < 1) return 'Năm không hợp lệ'; return null; } diff --git a/test/main_bloc_test.dart b/test/main_bloc_test.dart index 324bff7..b39cdca 100644 --- a/test/main_bloc_test.dart +++ b/test/main_bloc_test.dart @@ -1,5 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:licham/main_bloc.dart'; +import 'package:licham/main_event.dart'; import 'package:licham/main_state.dart'; void main() { @@ -25,4 +26,138 @@ void main() { await mainBloc.close(); await future; }); + + group('solar to lunar conversion', () { + Future convertDate(DateTime solar) async { + final bloc = MainBloc(solar)..add(const AppStarted()); + final state = await bloc.stream.firstWhere((s) => s is DateUpdate); + await bloc.close(); + return state as DateUpdate; + } + + group('Tết Nguyên Đán (Lunar New Year = 1/1)', () { + test('2024-02-10 is Mùng 1 Tết Giáp Thìn', () async { + final state = await convertDate(DateTime(2024, 2, 10)); + expect(state.lunar!.day, 1); + expect(state.lunar!.month, 1); + expect(state.lunar!.year, 2024); + expect(state.lunar!.canchiYear, 'Giáp Thìn'); + }); + + test('2025-01-29 is Mùng 1 Tết Ất Tỵ', () async { + final state = await convertDate(DateTime(2025, 1, 29)); + expect(state.lunar!.day, 1); + expect(state.lunar!.month, 1); + expect(state.lunar!.year, 2025); + expect(state.lunar!.canchiYear, 'Ất Tỵ'); + }); + + test('2026-02-17 is Mùng 1 Tết Bính Ngọ', () async { + final state = await convertDate(DateTime(2026, 2, 17)); + expect(state.lunar!.day, 1); + expect(state.lunar!.month, 1); + expect(state.lunar!.year, 2026); + expect(state.lunar!.canchiYear, 'Bính Ngọ'); + }); + + test('2023-01-22 is Mùng 1 Tết Quý Mão', () async { + final state = await convertDate(DateTime(2023, 1, 22)); + expect(state.lunar!.day, 1); + expect(state.lunar!.month, 1); + expect(state.lunar!.year, 2023); + expect(state.lunar!.canchiYear, 'Quý Mão'); + }); + }); + + group('Rằm Trung Thu (Mid-Autumn = 15/8)', () { + test('2024-09-17 is Rằm tháng Tám', () async { + final state = await convertDate(DateTime(2024, 9, 17)); + expect(state.lunar!.day, 15); + expect(state.lunar!.month, 8); + }); + + test('2025-10-06 is Rằm tháng Tám', () async { + final state = await convertDate(DateTime(2025, 10, 6)); + expect(state.lunar!.day, 15); + expect(state.lunar!.month, 8); + }); + }); + + test('emits auspicious hours', () async { + final state = await convertDate(DateTime(2024, 2, 10)); + expect(state.lunar!.hours, isNotNull); + expect(state.lunar!.hours, isNotEmpty); + }); + + test('emits Can Chi for day and month', () async { + final state = await convertDate(DateTime(2024, 2, 10)); + expect(state.lunar!.canChiDay, isNotNull); + expect(state.lunar!.canChiMonth, isNotNull); + }); + }); + + group('date navigation', () { + test('next day advances solar date', () async { + final bloc = MainBloc(DateTime(2024, 2, 10))..add(const AppStarted()); + + final states = []; + final sub = bloc.stream + .where((s) => s is DateUpdate) + .cast() + .listen(states.add); + + await Future.delayed(const Duration(milliseconds: 100)); + bloc.add(const NextSelected()); + await Future.delayed(const Duration(milliseconds: 100)); + + expect(states.length, greaterThanOrEqualTo(2)); + expect(states.last.solar!.day, 11); + expect(states.last.solar!.month, 2); + + await sub.cancel(); + await bloc.close(); + }); + + test('previous day goes back', () async { + final bloc = MainBloc(DateTime(2024, 2, 10))..add(const AppStarted()); + + final states = []; + final sub = bloc.stream + .where((s) => s is DateUpdate) + .cast() + .listen(states.add); + + await Future.delayed(const Duration(milliseconds: 100)); + bloc.add(const PreviousSelected()); + await Future.delayed(const Duration(milliseconds: 100)); + + expect(states.length, greaterThanOrEqualTo(2)); + expect(states.last.solar!.day, 9); + expect(states.last.solar!.month, 2); + + await sub.cancel(); + await bloc.close(); + }); + + test('solar date selection updates lunar', () async { + final bloc = MainBloc(DateTime(2024))..add(const AppStarted()); + + final states = []; + final sub = bloc.stream + .where((s) => s is DateUpdate) + .cast() + .listen(states.add); + + await Future.delayed(const Duration(milliseconds: 100)); + bloc.add(SolarSelected(solar: DateTime(2024, 2, 10))); + await Future.delayed(const Duration(milliseconds: 100)); + + expect(states.last.lunar!.day, 1); + expect(states.last.lunar!.month, 1); + expect(states.last.lunar!.year, 2024); + + await sub.cancel(); + await bloc.close(); + }); + }); }