=pod

=encoding UTF-8

=head1 NAME

Time::Str::Util - Binary search and time zone database utilities

=head1 SYNOPSIS

    use Time::Str::Util qw( find_tzdb_directory
                            valid_tzdb_timezone valid_posix_timezone
                            lower_bound upper_bound range_bounds );

    # Locate system zoneinfo directory
    my $tzdir = find_tzdb_directory();  # /usr/share/zoneinfo or undef

    # Validate timezone names
    valid_tzdb_timezone('America/New_York');           # true
    valid_posix_timezone('EST5EDT,M3.2.0,M11.1.0');   # true

    my @sorted = (10, 20, 30, 40, 50);

    my $i = lower_bound(\@sorted, 25);  # 2 (first element >= 25)
    my $j = upper_bound(\@sorted, 30);  # 3 (first element >  30)

    # With optional bounds
    my $k = lower_bound(\@sorted, 25, 1, 4);  # search within [1, 4)

    # Range query: indices of elements in [15, 35]
    my ($lo, $hi) = range_bounds(\@sorted, 15, 35);  # (1, 3)

=head1 DESCRIPTION

This module provides binary search functions for sorted integer arrays
and utilities for locating the system's IANA Time Zone Database.
All functions are exportable on request. Use C<:all> to import
everything.

=head1 FUNCTIONS

=head2 Binary Search

=head3 lower_bound

    my $index = lower_bound($arrayref, $value);
    my $index = lower_bound($arrayref, $value, $lo, $hi);

Returns the index of the first element in the sorted array that is
greater than or equal to C<$value>. If all elements are less than
C<$value>, returns the length of the array (one past the last index).

Optional C<$lo> and C<$hi> parameters restrict the search to the
half-open range C<[$lo, $hi)>. Defaults to C<[0, length)>.

=head3 range_bounds

    my ($lo, $hi) = range_bounds($arrayref, $min_value, $max_value);

Returns a pair of indices C<($lo, $hi)> defining the half-open range
C<[$lo, $hi)> of elements within C<[$min_value, $max_value]>.
C<$lo> is the index of the first element E<gt>= C<$min_value> (via
binary search), and C<$hi> is the index of the first element
E<gt> C<$max_value> (via linear scan from C<$lo>).

This is optimized for cases where few elements fall within the range,
as the linear scan avoids a second binary search.

Croaks if C<$min_value> E<gt> C<$max_value>.

=head3 upper_bound

    my $index = upper_bound($arrayref, $value);
    my $index = upper_bound($arrayref, $value, $lo, $hi);

Returns the index of the first element in the sorted array that is
strictly greater than C<$value>. If all elements are less than or
equal to C<$value>, returns the length of the array.

Optional C<$lo> and C<$hi> parameters restrict the search to the
half-open range C<[$lo, $hi)>. Defaults to C<[0, length)>.

=head2 Time Zone Database

=head3 find_tzdb_directory

    my $dir = find_tzdb_directory();

Returns the path to the system's IANA Time Zone Database directory
(zoneinfo), or C<undef> if no valid directory is found.

The search order is:

=over 4

=item 1. C<$ENV{TZDIR}> if set and the directory exists.

=item 2. Well-known paths: F</usr/share/zoneinfo>, F</usr/lib/zoneinfo>,
F</usr/share/lib/zoneinfo>, F</etc/zoneinfo>, F</usr/share/zoneinfo.default>.

=item 3. macOS: F</var/db/timezone/zoneinfo> (symlink to the active version).

=back

A candidate directory is accepted only if it exists and contains a
F<UTC> file.

=head2 Validation

=head3 valid_tzdb_timezone

    my $bool = valid_tzdb_timezone($string);

Returns true if C<$string> is structurally valid as an IANA Time Zone
Database timezone name (e.g., C<America/New_York>, C<Europe/Stockholm>,
C<Etc/GMT+5>, C<UTC>).

A valid name consists of one or more path components separated by C</>.
Each component starts with a letter, followed by letters or digits,
with optional C<_>, C<+>, or C<-> separators introducing additional
alphanumeric segments.

This is a structural check only; it does not verify that the timezone
exists in the database.

=head3 valid_posix_timezone

    my $bool = valid_posix_timezone($string);

Returns true if C<$string> is structurally valid as a POSIX TZ string
(IEEE Std 1003.1).

A POSIX TZ string has the form:

    std offset [dst [offset] , start [/time] , end [/time]]

Examples:

    EST5EDT,M3.2.0,M11.1.0          # US Eastern
    CET-1CEST,M3.5.0/2,M10.5.0/3   # Central European
    UTC0                             # Fixed UTC

Transition rules may use C<Mm.w.d> (month/week/weekday), C<Jn>
(Julian day excluding Feb 29), or C<n> (zero-based Julian day
including Feb 29) forms.

This validates the syntactic structure of the string only, not the
semantic validity of field values (e.g., month ranges, day ranges).
Quoted timezone designations (C<< <+05> >>) as used in TZif extensions
are not part of the POSIX standard and are not accepted.

=head1 SEE ALSO

L<Time::Str>

=head1 AUTHOR

Christian Hansen

=head1 COPYRIGHT AND LICENSE

Copyright (C) 2026 by Christian Hansen

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.

=cut
