diff --git a/lib/extensions.dart b/lib/extensions.dart index ed7c74b..d931e12 100644 --- a/lib/extensions.dart +++ b/lib/extensions.dart @@ -19,14 +19,18 @@ extension Concept2DateExtension on DateTime { } Uint8List toBytes() { - List bytes = []; + int yearOffset = year - 2000; - // int yearoffset = (year - 2000); - // bytes.add( (day & 0x0F)<<4 | month & 0x0F); + int dateValue = (yearOffset << 9) | (day << 4) | month; - // int day_year_byte = (day & 0x1F) | ; + Uint8List dateBytes = + dateValue.toBytes(fillBytes: 2, endian: Endian.little); - return Uint8List.fromList(bytes); + return Uint8List.fromList([ + ...dateBytes, + minute, + hour, + ]); } } diff --git a/lib/helpers.dart b/lib/helpers.dart index 86109f9..fd14afe 100644 --- a/lib/helpers.dart +++ b/lib/helpers.dart @@ -10,14 +10,17 @@ import 'models/workout.dart'; /// Concept2's structure is date LO, Date HI, Time LO, Time HI /// see also https://www.c2forum.com/viewtopic.php?f=15&t=200769 DateTime timeFromBytes(Uint8List bytes) { - int date = CsafeIntExtension.fromBytes(bytes.sublist(0, 2)); + int date = + CsafeIntExtension.fromBytes(bytes.sublist(0, 2), endian: Endian.little); int month = date & 0x0F; int day = (date >> 4) & 0x1F; int year = (date >> 9) & 0x7f; - int minutes = CsafeIntExtension.fromBytes(bytes.sublist(2, 3)); - int hours = CsafeIntExtension.fromBytes(bytes.sublist(3, 4)); + int minutes = + CsafeIntExtension.fromBytes(bytes.sublist(2, 3), endian: Endian.little); + int hours = + CsafeIntExtension.fromBytes(bytes.sublist(3, 4), endian: Endian.little); return DateTime(year + 2000, month, day, hours, minutes); } @@ -44,17 +47,34 @@ WorkoutGoal? guessReasonableSplit(WorkoutGoal goal) { //https://stackoverflow.com/questions/54852585/how-to-convert-a-duration-like-string-to-a-real-duration-in-flutter Duration parseDuration(String s) { + final parts = s.split(':'); + if (parts.length < 2 || parts.length > 3) { + throw const FormatException('Invalid duration format'); + } + + final secString = parts.last; + final sec = double.tryParse(secString); + if (sec == null) { + throw const FormatException('Invalid seconds value'); + } + + final minString = parts[parts.length - 2]; + final minutes = int.tryParse(minString); + if (minutes == null) { + throw const FormatException('Invalid minutes value'); + } + int hours = 0; - int minutes = 0; - int micros; - List parts = s.split(':'); - if (parts.length > 2) { - hours = int.parse(parts[parts.length - 3]); + if (parts.length == 3) { + hours = int.tryParse(parts.first) ?? + (throw const FormatException('Invalid hours value')); } - if (parts.length > 1) { - minutes = int.parse(parts[parts.length - 2]); + + if (hours < 0 || minutes < 0 || sec < 0) { + throw RangeError('Duration values must be non-negative'); } - micros = (double.parse(parts[parts.length - 1]) * 1000000).toInt(); + + final micros = (sec * Duration.microsecondsPerSecond).round(); return Duration(hours: hours, minutes: minutes, microseconds: micros); } @@ -73,6 +93,9 @@ String durationToSplit(Duration d) { // source: https://www.concept2.com/indoor-rowers/training/calculators/watts-calculator double splitToWatts(Duration split) { + if (split.inMilliseconds <= 0) { + throw RangeError('split must be positive'); + } double secondsperMeter = split.inMilliseconds / Duration.millisecondsPerSecond; var rawWatts = (2.8 / pow(secondsperMeter / 500, 3)); @@ -82,6 +105,9 @@ double splitToWatts(Duration split) { // pace = ³√(2.80/watts); pace = seconds per meter // source: https://www.concept2.com/indoor-rowers/training/calculators/watts-calculator String wattsToSplit(double watts) { + if (watts < 0) { + throw RangeError('watts must be non-negative'); + } if (watts == 0) { return "0:00.0"; } diff --git a/test/extensions_test.dart b/test/extensions_test.dart index 6b096f9..bd03318 100644 --- a/test/extensions_test.dart +++ b/test/extensions_test.dart @@ -7,16 +7,16 @@ void main() { group("Concept2DateExtension - ", () { test('converting a time from bytes', () { final bytes = Uint8List.fromList([156, 42, 1, 14]); //date 10908 - expect( - Concept2DateExtension.fromBytes(bytes), DateTime(2021, 12, 9, 14, 1)); - // expect(CsafeIntExtension.fromBytes(bytes, Endian.little), 2147483648); + final date = Concept2DateExtension.fromBytes(bytes); + expect(date, DateTime(2021, 12, 9, 14, 1)); + expect(date.toBytes(), bytes); }); test('converting another time from bytes', () { final bytes = Uint8List.fromList([241, 45, 1, 14]); - expect( - Concept2DateExtension.fromBytes(bytes), DateTime(2022, 1, 31, 14, 1)); - // expect(CsafeIntExtension.fromBytes(bytes, Endian.little), 2147483648); + final date = Concept2DateExtension.fromBytes(bytes); + expect(date, DateTime(2022, 1, 31, 14, 1)); + expect(date.toBytes(), bytes); }); test('converting a date from a verifiable source from bytes', () { @@ -25,26 +25,27 @@ void main() { // 00011101 10010100 00010000 00001001 // final bytes = Uint8List.fromList([29, 148, 16, 9]); final bytes = Uint8List.fromList([148, 29, 9, 16]); - - expect(Concept2DateExtension.fromBytes(bytes), - DateTime(2014, 4, 25, 16, 09)); + final date = Concept2DateExtension.fromBytes(bytes); + expect(date, DateTime(2014, 4, 25, 16, 09)); // 25th April 2014 @ 16:09 (4:09pm) + expect(date.toBytes(), bytes); }); test('converting a date in 2022 from bytes', () { final bytes = Uint8List.fromList([133, 44, 14, 14]); - expect( - Concept2DateExtension.fromBytes(bytes), DateTime(2022, 5, 8, 14, 14)); + final date = Concept2DateExtension.fromBytes(bytes); + expect(date, DateTime(2022, 5, 8, 14, 14)); + expect(date.toBytes(), bytes); }); }); group("Concept2DurationExtension - ", () { - // TODO: need bytes to use as test case - // test('converting a duration from bytes', () { - // // TODO: fixme, my endianness is backwards - // final bytes = Uint8List.fromList([42, 156, 1, 14]); //date 10908 - // expect( - // Concept2DurationExtension.fromBytes(bytes), DateTime(2021, 12, 9, 14, 1)); - // }); + test('converts a duration from bytes and back again', () { + // 1 minute, 30.5 seconds -> 90500 ms -> 9050 hundred of a second + final bytes = Uint8List.fromList([90, 35, 0]); + final duration = Concept2DurationExtension.fromBytes(bytes); + expect(duration, Duration(minutes: 1, seconds: 30, milliseconds: 500)); + expect(duration.toBytes(), bytes); + }); }); } diff --git a/test/helpers_test.dart b/test/helpers_test.dart index adbdf45..3ffd04a 100644 --- a/test/helpers_test.dart +++ b/test/helpers_test.dart @@ -1,10 +1,10 @@ +import 'dart:typed_data'; + import 'package:c2bluetooth/helpers.dart'; import 'package:c2bluetooth/models/workout.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { - - group("guessReasonableSplit - ", () { test("test with reasonable distance", () { expect(guessReasonableSplit(WorkoutGoal.meters(2000)), @@ -35,10 +35,13 @@ void main() { test('3:00.2', () { expect(parseDuration("3:00.2"), Duration(minutes: 3, milliseconds: 200)); }); - // TODO: Update errors for splitToWatts() - // test('When invalid split format, should throw error', () { - // expect(() => splitToWatts('0'), throwsFormatException); - // }); + test('throws FormatException on malformed string', () { + expect(() => parseDuration('foo'), throwsFormatException); + }); + + test('throws RangeError on negative values', () { + expect(() => parseDuration('-1:00.0'), throwsRangeError); + }); }); group('splitToWatts -', () { @@ -59,10 +62,12 @@ void main() { var result = splitToWatts(Duration(minutes: 3)); expect(result, 60); }); - // TODO: Update errors for splitToWatts() - // test('When invalid split format, should throw error', () { - // expect(() => splitToWatts('0'), throwsFormatException); - // }); + test('throws RangeError when split is non-positive', () { + expect(() => splitToWatts(Duration.zero), throwsRangeError); + }); + test('throws RangeError when split is negative', () { + expect(() => splitToWatts(Duration(seconds: -5)), throwsRangeError); + }); }); group('wattsToSplit -', () { test('When watts is 179, should be 2:05.0', () { @@ -85,5 +90,34 @@ void main() { var result = wattsToSplit(55555); expect(result, '0:18.5'); }); + test('throws RangeError when watts is negative', () { + expect(() => wattsToSplit(-10), throwsRangeError); + }); + }); + group('timeFromBytes -', () { + test('should convert PM date bytes to DateTime', () { + final bytes = Uint8List.fromList([156, 42, 1, 14]); + expect(timeFromBytes(bytes), DateTime(2021, 12, 9, 14, 1)); + }); + + test('should convert another valid byte sequence', () { + final bytes = Uint8List.fromList([241, 45, 1, 14]); + expect(timeFromBytes(bytes), DateTime(2022, 1, 31, 14, 1)); + }); + }); + + group('durationToSplit -', () { + test('converts a 2:05 duration', () { + expect(durationToSplit(Duration(minutes: 2, seconds: 5)), '2:05.0'); + }); + + test('handles seconds only', () { + expect(durationToSplit(Duration(seconds: 30)), '0:30.0'); + }); + + test('handles fractional seconds', () { + expect( + durationToSplit(Duration(minutes: 1, milliseconds: 900)), '1:00.9'); + }); }); }