diff options
| -rw-r--r-- | challenge-347/lubos-kolouch/README | 32 | ||||
| -rw-r--r-- | challenge-347/lubos-kolouch/perl/ch-1.pl | 112 | ||||
| -rw-r--r-- | challenge-347/lubos-kolouch/perl/ch-2.pl | 113 | ||||
| -rw-r--r-- | challenge-347/lubos-kolouch/python/ch-1.py | 100 | ||||
| -rw-r--r-- | challenge-347/lubos-kolouch/python/ch-2.py | 71 | ||||
| -rw-r--r-- | challenge-348/lubos-kolouch/README | 32 | ||||
| -rw-r--r-- | challenge-348/lubos-kolouch/perl/ch-1.pl | 92 | ||||
| -rw-r--r-- | challenge-348/lubos-kolouch/perl/ch-2.pl | 105 | ||||
| -rw-r--r-- | challenge-348/lubos-kolouch/python/ch-1.py | 63 | ||||
| -rw-r--r-- | challenge-348/lubos-kolouch/python/ch-2.py | 85 |
10 files changed, 773 insertions, 32 deletions
diff --git a/challenge-347/lubos-kolouch/README b/challenge-347/lubos-kolouch/README index 7f47843cb2..8b814b0ccb 100644 --- a/challenge-347/lubos-kolouch/README +++ b/challenge-347/lubos-kolouch/README @@ -1,26 +1,26 @@ -# Perl Weekly Challenge 346 Solutions +# Perl Weekly Challenge 347 Solutions -Perl and Python implementations for both tasks of Weekly Challenge 346. +Perl and Python implementations for both tasks of Weekly Challenge 347. -## Task 1: Longest Parenthesis +## Task 1: Format Date -Determine the length of the longest valid parenthesis substring contained -within an input built from `(` and `)`. +Convert strings like `10th Nov 2025` into ISO form `2025-11-10` while validating +the provided day, month, and year values. -- **Perl (`perl/ch-1.pl`)**: Uses a stack of indices with type-checked input - to track unmatched parentheses and compute the maximum span. -- **Python (`python/ch-1.py`)**: Mirrors the stack-based pass with explicit - type hints and `unittest` coverage for the specification data. +- **Perl (`perl/ch-1.pl`)**: Parses the ordinal components with type-checked + input and enforces suffix/month/year constraints before formatting. +- **Python (`python/ch-1.py`)**: Uses the same parsing logic with type + annotations and `unittest` coverage for each provided example. -## Task 2: Magic Expression +## Task 2: Format Phone Number -Insert `+`, `-`, and `*` between the digits of the supplied string so the -expression evaluates to the target integer. +Normalize phone numbers that contain digits, spaces, and dashes by removing +separators and regrouping the digits according to the challenge rules. -- **Perl (`perl/ch-2.pl`)**: Performs depth-first search with precedence-aware - bookkeeping, returning every lexicographically sorted solution. -- **Python (`python/ch-2.py`)**: Applies the same backtracking strategy with - typed helpers and unit tests bound to the official examples. +- **Perl (`perl/ch-2.pl`)**: Validates allowed characters, strips separators, + and creates 3-digit blocks with special handling for the trailing digits. +- **Python (`python/ch-2.py`)**: Mirrors the greedy regrouping approach and + includes type hints plus example-driven unit tests. ## Running the Solutions diff --git a/challenge-347/lubos-kolouch/perl/ch-1.pl b/challenge-347/lubos-kolouch/perl/ch-1.pl new file mode 100644 index 0000000000..13d82e2e15 --- /dev/null +++ b/challenge-347/lubos-kolouch/perl/ch-1.pl @@ -0,0 +1,112 @@ +#!/usr/bin/env perl +use v5.38; +use warnings; +use feature 'signatures'; +no warnings 'experimental::signatures'; +## no critic (Subroutines::ProhibitSubroutinePrototypes) + +use Type::Params qw(compile); +use Types::Standard qw(Str); + +=pod + +=head1 NAME + +ch-1.pl - Format Date (Perl Weekly Challenge 347) + +=head1 SYNOPSIS + + perl ch-1.pl "1st Jan 2025" + perl ch-1.pl # executes the example tests + +=head1 DESCRIPTION + +Parses an input such as C<10th Nov 2025> and emits it in ISO form +C<2025-11-10>. Validation follows the specification-provided sets of days, +months, and years. + +=cut + +my %MONTH_INDEX = ( + Jan => 1, + Feb => 2, + Mar => 3, + Apr => 4, + May => 5, + Jun => 6, + Jul => 7, + Aug => 8, + Sep => 9, + Oct => 10, + Nov => 11, + Dec => 12, +); + +sub format_date ($input) { + state $check = compile(Str); + ($input) = $check->($input); + $input =~ s/^\s+|\s+$//g; + + my ( $day, $suffix, $month_name, $year ) = + $input =~ /\A(\d{1,2})(st|nd|rd|th)\s+([A-Za-z]{3})\s+(\d{4})\z/ + or die 'Invalid date format'; + + my $day_num = $day + 0; + die 'Day out of range' unless 1 <= $day_num && $day_num <= 31; + die 'Invalid ordinal suffix' unless _suffix_for($day_num) eq $suffix; + + my $month_num = $MONTH_INDEX{$month_name} // die 'Unknown month abbreviation'; + + my $year_num = $year + 0; + die 'Year out of range' unless 1900 <= $year_num && $year_num <= 2100; + + return sprintf '%04d-%02d-%02d', $year_num, $month_num, $day_num; +} + +sub _suffix_for ($day) { + return 'th' if $day % 100 >= 11 && $day % 100 <= 13; + return ( 'th', 'st', 'nd', 'rd', 'th', 'th', 'th', 'th', 'th', 'th' )[ $day % 10 ]; +} + +sub _run_cli (@args) { + if ( !@args ) { + _run_tests(); + return; + } + + die qq{Usage: perl $0 "1st Jan 2025"\n} if @args != 1; + my $input = $args[0]; + say qq{Input: \$str = "$input"}; + my $formatted = format_date($input); + say qq{Output: "$formatted"}; +} + +sub _run_tests { + require Test::More; + Test::More->import(); + + my @cases = ( + { label => 'Example 1', str => '1st Jan 2025', expected => '2025-01-01' }, + { label => 'Example 2', str => '22nd Feb 2025', expected => '2025-02-22' }, + { label => 'Example 3', str => '15th Apr 2025', expected => '2025-04-15' }, + { label => 'Example 4', str => '23rd Oct 2025', expected => '2025-10-23' }, + { label => 'Example 5', str => '31st Dec 2025', expected => '2025-12-31' }, + ); + + Test::More::plan( tests => scalar @cases ); + for my $case (@cases) { + my $got = format_date( $case->{str} ); + Test::More::is( $got, $case->{expected}, $case->{label} ); + } +} + +_run_cli(@ARGV); + +=head1 FUNCTIONS + +=head2 format_date($input) + +Returns the ISO-formatted representation of C<$input>. Throws an exception when +the supplied string falls outside the allowed day, month, or year sets. + +=cut diff --git a/challenge-347/lubos-kolouch/perl/ch-2.pl b/challenge-347/lubos-kolouch/perl/ch-2.pl new file mode 100644 index 0000000000..d936af0d77 --- /dev/null +++ b/challenge-347/lubos-kolouch/perl/ch-2.pl @@ -0,0 +1,113 @@ +#!/usr/bin/env perl +use v5.38; +use warnings; +use feature 'signatures'; +no warnings 'experimental::signatures'; +## no critic (Subroutines::ProhibitSubroutinePrototypes) + +use Type::Params qw(compile); +use Types::Standard qw(Str); + +=pod + +=head1 NAME + +ch-2.pl - Format Phone Number (Perl Weekly Challenge 347) + +=head1 SYNOPSIS + + perl ch-2.pl "12 345-6789" + perl ch-2.pl # runs the embedded tests + +=head1 DESCRIPTION + +Normalizes a phone number consisting of digits, spaces, and dashes into blocks +separated by single dashes according to the rules from the challenge: + +=over 4 + +=item * +Remove all spaces and dashes. + +=item * +Group from the left using blocks of length 3. + +=item * +If the final chunk would contain a single digit, rebalance the last two blocks +into two groups of length 2. + +=back + +=cut + +sub format_phone_number ($input) { + state $check = compile(Str); + ($input) = $check->($input); + die 'Phone number requires at least two digits' unless $input =~ /\d.*\d/; + die 'Invalid characters in phone number' + if $input =~ /[^0-9\s-]/; + + my $digits = $input =~ s/[\s-]//gr; + my $length = length $digits; + die 'Phone number requires at least two digits' if $length < 2; + + my @blocks; + my $pos = 0; + while ( $length - $pos > 4 ) { + push @blocks, substr $digits, $pos, 3; + $pos += 3; + } + + my $remaining = substr $digits, $pos; + if ( length $remaining == 4 ) { + push @blocks, substr( $remaining, 0, 2 ), substr( $remaining, 2, 2 ); + } + elsif ( length $remaining ) { + push @blocks, $remaining; + } + + return join '-', @blocks; +} + +sub _run_cli (@args) { + if ( !@args ) { + _run_tests(); + return; + } + + die qq{Usage: perl $0 "<phone number>"\n} if @args != 1; + my $input = $args[0]; + say qq{Input: \$phone = "$input"}; + my $formatted = format_phone_number($input); + say qq{Output: "$formatted"}; +} + +sub _run_tests { + require Test::More; + Test::More->import(); + + my @cases = ( + { label => 'Example 1', phone => '1-23-45-6', expected => '123-456' }, + { label => 'Example 2', phone => '1234', expected => '12-34' }, + { label => 'Example 3', phone => '12 345-6789', expected => '123-456-789' }, + { label => 'Example 4', phone => '123 4567', expected => '123-45-67' }, + { label => 'Example 5', phone => '123 456-78', expected => '123-456-78' }, + ); + + Test::More::plan( tests => scalar @cases ); + for my $case (@cases) { + my $got = format_phone_number( $case->{phone} ); + Test::More::is( $got, $case->{expected}, $case->{label} ); + } +} + +_run_cli(@ARGV); + +=head1 FUNCTIONS + +=head2 format_phone_number($input) + +Returns the normalized representation of C<$input>. Throws an exception unless +the input contains only digits, spaces, and dashes and at least two digits. + +=cut diff --git a/challenge-347/lubos-kolouch/python/ch-1.py b/challenge-347/lubos-kolouch/python/ch-1.py new file mode 100644 index 0000000000..62423b7a3a --- /dev/null +++ b/challenge-347/lubos-kolouch/python/ch-1.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +"""Format Date - Perl Weekly Challenge 347 task 1.""" + +from __future__ import annotations + +from typing import Final +from collections.abc import Sequence +import re +import sys +import unittest + +MONTHS: Final[dict[str, int]] = { + "Jan": 1, + "Feb": 2, + "Mar": 3, + "Apr": 4, + "May": 5, + "Jun": 6, + "Jul": 7, + "Aug": 8, + "Sep": 9, + "Oct": 10, + "Nov": 11, + "Dec": 12, +} + +DATE_PATTERN: Final[re.Pattern[str]] = re.compile( + r"^(?P<day>\d{1,2})(?P<suffix>st|nd|rd|th)\s+(?P<month>[A-Za-z]{3})\s+(?P<year>\d{4})$" +) + + +def _suffix_for(day: int) -> str: + if 11 <= day % 100 <= 13: + return "th" + suffixes = ["th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th"] + return suffixes[day % 10] + + +def format_date(text: str) -> str: + """Convert strings like '10th Nov 2025' to '2025-11-10'.""" + match = DATE_PATTERN.match(text.strip()) + if not match: + raise ValueError("Invalid date format") + + day = int(match.group("day")) + suffix = match.group("suffix") + month_name = match.group("month") + year = int(match.group("year")) + + if not 1 <= day <= 31: + raise ValueError("Day out of range") + if _suffix_for(day) != suffix: + raise ValueError("Invalid ordinal suffix") + + month = MONTHS.get(month_name) + if month is None: + raise ValueError("Unknown month abbreviation") + if not 1900 <= year <= 2100: + raise ValueError("Year out of range") + + return f"{year:04d}-{month:02d}-{day:02d}" + + +class FormatDateExamples(unittest.TestCase): + """Specification-provided examples.""" + + def test_example_1(self) -> None: + self.assertEqual(format_date("1st Jan 2025"), "2025-01-01") + + def test_example_2(self) -> None: + self.assertEqual(format_date("22nd Feb 2025"), "2025-02-22") + + def test_example_3(self) -> None: + self.assertEqual(format_date("15th Apr 2025"), "2025-04-15") + + def test_example_4(self) -> None: + self.assertEqual(format_date("23rd Oct 2025"), "2025-10-23") + + def test_example_5(self) -> None: + self.assertEqual(format_date("31st Dec 2025"), "2025-12-31") + + +def main(argv: Sequence[str] | None = None) -> None: + """Command-line interface: run tests or format a provided string.""" + args = list(sys.argv[1:] if argv is None else argv) + if not args: + unittest.main(argv=[sys.argv[0]]) + return + + if len(args) != 1: + raise SystemExit('Usage: python3 ch-1.py "1st Jan 2025"') + + value = args[0] + print(f'Input: $str = "{value}"') + result = format_date(value) + print(f'Output: "{result}"') + + +if __name__ == "__main__": + main() diff --git a/challenge-347/lubos-kolouch/python/ch-2.py b/challenge-347/lubos-kolouch/python/ch-2.py new file mode 100644 index 0000000000..8a96a2712b --- /dev/null +++ b/challenge-347/lubos-kolouch/python/ch-2.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +"""Format Phone Number - Perl Weekly Challenge 347 task 2.""" + +from __future__ import annotations + +from collections.abc import Sequence +import sys +import unittest + + +def format_phone_number(text: str) -> str: + """Group phone digits from the left using 3-digit blocks with special tail handling.""" + if any(ch for ch in text if ch not in "0123456789 -"): + raise ValueError("Invalid characters in phone number") + + digits = "".join(ch for ch in text if ch.isdigit()) + if len(digits) < 2: + raise ValueError("Phone number requires at least two digits") + + blocks: list[str] = [] + idx = 0 + while len(digits) - idx > 4: + blocks.append(digits[idx:idx + 3]) + idx += 3 + + tail = digits[idx:] + if len(tail) == 4: + blocks.extend([tail[:2], tail[2:]]) + elif tail: + blocks.append(tail) + + return "-".join(blocks) + + +class FormatPhoneNumberExamples(unittest.TestCase): + """Specification-provided examples.""" + + def test_example_1(self) -> None: + self.assertEqual(format_phone_number("1-23-45-6"), "123-456") + + def test_example_2(self) -> None: + self.assertEqual(format_phone_number("1234"), "12-34") + + def test_example_3(self) -> None: + self.assertEqual(format_phone_number("12 345-6789"), "123-456-789") + + def test_example_4(self) -> None: + self.assertEqual(format_phone_number("123 4567"), "123-45-67") + + def test_example_5(self) -> None: + self.assertEqual(format_phone_number("123 456-78"), "123-456-78") + + +def main(argv: Sequence[str] | None = None) -> None: + """Command-line interface.""" + args = list(sys.argv[1:] if argv is None else argv) + if not args: + unittest.main(argv=[sys.argv[0]]) + return + + if len(args) != 1: + raise SystemExit('Usage: python3 ch-2.py "12 345-6789"') + + phone = args[0] + print(f'Input: $phone = "{phone}"') + formatted = format_phone_number(phone) + print(f'Output: "{formatted}"') + + +if __name__ == "__main__": + main() diff --git a/challenge-348/lubos-kolouch/README b/challenge-348/lubos-kolouch/README index 7f47843cb2..e48d8e40b9 100644 --- a/challenge-348/lubos-kolouch/README +++ b/challenge-348/lubos-kolouch/README @@ -1,26 +1,26 @@ -# Perl Weekly Challenge 346 Solutions +# Perl Weekly Challenge 348 Solutions -Perl and Python implementations for both tasks of Weekly Challenge 346. +Perl and Python implementations for both tasks of Weekly Challenge 348. -## Task 1: Longest Parenthesis +## Task 1: String Alike -Determine the length of the longest valid parenthesis substring contained -within an input built from `(` and `)`. +Split an even-length string into two halves and verify that each half contains +the same, non-zero number of vowels. -- **Perl (`perl/ch-1.pl`)**: Uses a stack of indices with type-checked input - to track unmatched parentheses and compute the maximum span. -- **Python (`python/ch-1.py`)**: Mirrors the stack-based pass with explicit - type hints and `unittest` coverage for the specification data. +- **Perl (`perl/ch-1.pl`)**: Uses typed parameter validation and a single pass + over each half to count vowels before comparing the totals. +- **Python (`python/ch-1.py`)**: Provides the same logic with type annotations + and `unittest` coverage of the specification examples. -## Task 2: Magic Expression +## Task 2: Convert Time -Insert `+`, `-`, and `*` between the digits of the supplied string so the -expression evaluates to the target integer. +Determine the minimal number of operations required to turn the source 24-hour +timestamp into the target when you may add 1, 5, 15, or 60 minutes. -- **Perl (`perl/ch-2.pl`)**: Performs depth-first search with precedence-aware - bookkeeping, returning every lexicographically sorted solution. -- **Python (`python/ch-2.py`)**: Applies the same backtracking strategy with - typed helpers and unit tests bound to the official examples. +- **Perl (`perl/ch-2.pl`)**: Converts stamps to minutes-past-midnight with type + validation and applies a greedy decomposition over the permitted steps. +- **Python (`python/ch-2.py`)**: Uses the same helper logic, fully typed, along + with a unittest suite that exercises the provided scenarios. ## Running the Solutions diff --git a/challenge-348/lubos-kolouch/perl/ch-1.pl b/challenge-348/lubos-kolouch/perl/ch-1.pl new file mode 100644 index 0000000000..0d2975ab92 --- /dev/null +++ b/challenge-348/lubos-kolouch/perl/ch-1.pl @@ -0,0 +1,92 @@ +#!/usr/bin/env perl +use v5.38; +use warnings; +use feature 'signatures'; +no warnings 'experimental::signatures'; +## no critic (Subroutines::ProhibitSubroutinePrototypes) + +use Type::Params qw(compile); +use Types::Standard qw(Str); + +=pod + +=head1 NAME + +ch-1.pl - String Alike + +=head1 SYNOPSIS + + perl ch-1.pl <even_length_string> + perl ch-1.pl # runs the embedded tests + +=head1 DESCRIPTION + +Checks whether a provided even-length string can be split into two halves that +share the same non-zero number of vowels. The implementation keeps the logic in +L</string_alike> so it may be reused both by the command line interface and the +unit tests bundled at the end of the file. + +=cut + +my $STRING_CHECK = compile(Str); + +sub string_alike ($text) { + ($text) = $STRING_CHECK->($text); + my $length = length $text; + die 'Expected an even-length string' if $length % 2; + + my $half = $length / 2; + my $first_half = substr $text, 0, $half; + my $second_half = substr $text, $half; + my $first_count = ( $first_half =~ tr/AEIOUaeiou// ); + my $second_count = ( $second_half =~ tr/AEIOUaeiou// ); + + return $first_count > 0 && $first_count == $second_count; +} + +sub _run_cli (@args) { + if ( !@args ) { + _run_tests(); + return; + } + + die "Usage: perl $0 <even_length_string>\n" if @args != 1; + my $input = $args[0]; + say qq{Input: \$str = "$input"}; + my $result = string_alike($input) ? 'true' : 'false'; + say "Output: $result"; +} + +sub _run_tests { + require Test::More; + Test::More->import(); + + my @cases = ( + { label => 'Example 1', str => 'textbook', expected => 0 }, + { label => 'Example 2', str => 'book', expected => 1 }, + { label => 'Example 3', str => 'AbCdEfGh', expected => 1 }, + { label => 'Example 4', str => 'rhythmmyth', expected => 0 }, + { label => 'Example 5', str => 'UmpireeAudio', expected => 0 }, + ); + + Test::More::plan( tests => scalar @cases ); + + for my $case (@cases) { + my $got = string_alike( $case->{str} ) ? 1 : 0; + Test::More::is( $got, $case->{expected}, $case->{label} ); + } +} + +_run_cli(@ARGV); + +=pod + +=head1 FUNCTIONS + +=head2 string_alike($text) + +Returns a boolean indicating whether the halves of C<$text> have the same +positive count of vowels. Throws an exception when the input does not meet the +problem specification (e.g. odd length). + +=cut diff --git a/challenge-348/lubos-kolouch/perl/ch-2.pl b/challenge-348/lubos-kolouch/perl/ch-2.pl new file mode 100644 index 0000000000..463cbe7244 --- /dev/null +++ b/challenge-348/lubos-kolouch/perl/ch-2.pl @@ -0,0 +1,105 @@ +#!/usr/bin/env perl +use v5.38; +use warnings; +use feature 'signatures'; +no warnings 'experimental::signatures'; +## no critic (Subroutines::ProhibitSubroutinePrototypes) + +use Type::Params qw(compile); +use Types::Standard qw(StrMatch); + +=pod + +=head1 NAME + +ch-2.pl - Convert Time + +=head1 SYNOPSIS + + perl ch-2.pl <source> <target> + perl ch-2.pl # runs the embedded tests + +=head1 DESCRIPTION + +Computes the minimal number of allowed operations needed to transform a source +24-hour clock stamp into a target one by adding 1, 5, 15, or 60 minutes. The +greedy strategy works because every duration divides the next larger one. + +=cut + +my $STAMP_CHECK = compile( StrMatch [qr/\A\d\d:\d\d\z/] ); +my @OPERATIONS = ( 60, 15, 5, 1 ); + +sub convert_time ( $source, $target ) { + state $pair_check = compile( StrMatch [qr/\A\d\d:\d\d\z/], StrMatch [qr/\A\d\d:\d\d\z/], ); + ( $source, $target ) = $pair_check->( $source, $target ); + + my $start = _minutes_from_midnight($source); + my $end = _minutes_from_midnight($target); + my $diff = $end - $start; + $diff += 24 * 60 if $diff < 0; + + my $operations = 0; + for my $step (@OPERATIONS) { + $operations += int( $diff / $step ); + $diff %= $step; + } + + return $operations; +} + +sub _minutes_from_midnight ($stamp) { + ($stamp) = $STAMP_CHECK->($stamp); + my ( $hour, $minute ) = split /:/, $stamp, 2; + die 'Invalid hour component' if $hour < 0 || $hour > 23; + die 'Invalid minute component' if $minute < 0 || $minute > 59; + return ( $hour * 60 ) + $minute; +} + +sub _run_cli (@args) { + if ( !@args ) { + _run_tests(); + return; + } + + die "Usage: perl $0 <source> <target>\n" if @args != 2; + my ( $source, $target ) = @args; + say qq{Input: \$source = "$source"}; + say qq{ \$target = "$target"}; + my $steps = convert_time( $source, $target ); + say "Output: $steps"; +} + +sub _run_tests { + require Test::More; + Test::More->import(); + + my @cases = ( + { label => 'Example 1', source => '02:30', target => '02:45', expected => 1 }, + { label => 'Example 2', source => '11:55', target => '12:15', expected => 2 }, + { label => 'Example 3', source => '09:00', target => '13:00', expected => 4 }, + { label => 'Example 4', source => '23:45', target => '00:30', expected => 3 }, + { label => 'Example 5', source => '14:20', target => '15:25', expected => 2 }, + ); + + Test::More::plan( tests => scalar @cases ); + + for my $case (@cases) { + my $got = convert_time( $case->{source}, $case->{target} ); + Test::More::is( $got, $case->{expected}, $case->{label} ); + } +} + +_run_cli(@ARGV); + +=pod + +=head1 FUNCTIONS + +=head2 convert_time($source, $target) + +Returns the minimal number of permitted increments required to reach C<$target> +from C<$source>. Internally converts both timestamps to minutes past midnight +and applies a greedy decomposition. + +=cut diff --git a/challenge-348/lubos-kolouch/python/ch-1.py b/challenge-348/lubos-kolouch/python/ch-1.py new file mode 100644 index 0000000000..3a878a835a --- /dev/null +++ b/challenge-348/lubos-kolouch/python/ch-1.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +"""String Alike - Perl Weekly Challenge 348 task 1.""" + +from __future__ import annotations + +from typing import Final +from collections.abc import Sequence +import sys +import unittest + +VOWELS: Final[frozenset[str]] = frozenset("aeiouAEIOU") + + +def is_string_alike(text: str) -> bool: + """Return True when the halves of ``text`` share the same positive vowel count.""" + if len(text) % 2 != 0: + raise ValueError("Expected an even-length string") + + half = len(text) // 2 + left = text[:half] + right = text[half:] + left_count = sum(1 for char in left if char in VOWELS) + right_count = sum(1 for char in right if char in VOWELS) + return left_count > 0 and left_count == right_count + + +class StringAlikeExamples(unittest.TestCase): + """Example-based tests provided in the specification.""" + + def test_example_1(self) -> None: + self.assertFalse(is_string_alike("textbook")) + + def test_example_2(self) -> None: + self.assertTrue(is_string_alike("book")) + + def test_example_3(self) -> None: + self.assertTrue(is_string_alike("AbCdEfGh")) + + def test_example_4(self) -> None: + self.assertFalse(is_string_alike("rhythmmyth")) + + def test_example_5(self) -> None: + self.assertFalse(is_string_alike("UmpireeAudio")) + + +def main(argv: Sequence[str] | None = None) -> None: + """Command-line launcher.""" + args = list(sys.argv[1:] if argv is None else argv) + if not args: + unittest.main(argv=[sys.argv[0]]) + return + + if len(args) != 1: + raise SystemExit("Usage: python3 ch-1.py <even_length_string>") + + word = args[0] + print(f'Input: $str = "{word}"') + result = "true" if is_string_alike(word) else "false" + print(f"Output: {result}") + + +if __name__ == "__main__": + main() diff --git a/challenge-348/lubos-kolouch/python/ch-2.py b/challenge-348/lubos-kolouch/python/ch-2.py new file mode 100644 index 0000000000..ab0105c2de --- /dev/null +++ b/challenge-348/lubos-kolouch/python/ch-2.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +"""Convert Time - Perl Weekly Challenge 348 task 2.""" + +from __future__ import annotations + +from typing import Final +from collections.abc import Sequence +import sys +import unittest + +MINUTES_PER_DAY: Final[int] = 24 * 60 +ALLOWED_STEPS: Final[tuple[int, ...]] = (60, 15, 5, 1) + + +def minutes_from_midnight(stamp: str) -> int: + """Convert a HH:MM timestamp into total minutes past midnight.""" + if len(stamp) != 5 or stamp[2] != ":": + raise ValueError("Time must be in HH:MM format") + + hours_str, minutes_str = stamp.split(":", 1) + if not (hours_str.isdigit() and minutes_str.isdigit()): + raise ValueError("Time must only contain digits") + + hours = int(hours_str) + minutes = int(minutes_str) + if not (0 <= hours <= 23 and 0 <= minutes <= 59): + raise ValueError("Time components out of range") + + return hours * 60 + minutes + + +def convert_time(source: str, target: str) -> int: + """Return the minimal number of operations to transform source into target.""" + start = minutes_from_midnight(source) + end = minutes_from_midnight(target) + diff = end - start + if diff < 0: + diff += MINUTES_PER_DAY + + operations = 0 + remaining = diff + for step in ALLOWED_STEPS: + operations += remaining // step + remaining %= step + return operations + + +class ConvertTimeExamples(unittest.TestCase): + """Example-based tests for the specification.""" + + def test_example_1(self) -> None: + self.assertEqual(convert_time("02:30", "02:45"), 1) + + def test_example_2(self) -> None: + self.assertEqual(convert_time("11:55", "12:15"), 2) + + def test_example_3(self) -> None: + self.assertEqual(convert_time("09:00", "13:00"), 4) + + def test_example_4(self) -> None: + self.assertEqual(convert_time("23:45", "00:30"), 3) + + def test_example_5(self) -> None: + self.assertEqual(convert_time("14:20", "15:25"), 2) + + +def main(argv: Sequence[str] | None = None) -> None: + """Command-line launcher.""" + args = list(sys.argv[1:] if argv is None else argv) + if not args: + unittest.main(argv=[sys.argv[0]]) + return + + if len(args) != 2: + raise SystemExit("Usage: python3 ch-2.py <source> <target>") + + source, target = args + print(f'Input: $source = "{source}"') + print(f' $target = "{target}"') + steps = convert_time(source, target) + print(f"Output: {steps}") + + +if __name__ == "__main__": + main() |
