package Prty::Duration;
use base qw/Prty::Object/;

use strict;
use warnings;

our $VERSION = 1.111;

use Prty::Option;
use Prty::Math;

# -----------------------------------------------------------------------------

=encoding utf8

=head1 NAME

Prty::Duration - Rechnen und Konvertieren von Zeiträumen

=head1 BASE CLASS

L<Prty::Object>

=head1 DESCRIPTION

Ein Objekt der Klasse repräsentiert eine Zeitdauer, die intern als
numerischer Wert (Sekunden mit Nachkommastellen) gespeichert wird.

Als externe Repäsentation wird die Darstellung

    DdHhMmS.Xs

verwendet, wobei

    D = Anzahl Tage
    H = Anzahl Stunden
    M = Anzahl Minuten
    S = Anzahl Sekunden
    X = Bruchteil einer Sekunde

Es gelten folgende Eingenschaften:

=over 2

=item *

führende Anteile bis auf Ss fehlen, wenn sie 0 sind

=item *

X fehlt, wenn 0

=item *

Ss erscheint immer, auch bei 0 Sekunden

=item *

die leere Zeichenkette ('') oder undef entspricht 0 Sekunden

=item *

der Sekundenanteil kann Nachkommastellen haben

=back

=head1 METHODS

=head2 Konstruktor

=head3 new() - Konstruktor

=head4 Synopsis

    $dur = Prty::Duration->new($sec);
    $dur = Prty::Duration->new($str);

=head4 Description

Instantiiere ein Zeitdauer-Objekt und liefere einen Referenz auf
dieses Objekt zurück. Die Zeitdauer kann als numerischer Wert $sec
oder als Zeichenkette $str angegeben werden.

=cut

# -----------------------------------------------------------------------------

sub new {
    my $class = shift;
    my $sec = shift || 0;

    if ($sec =~ tr/a-z//) {
        $sec = $class->stringToSeconds($sec);
    }

    return bless \$sec,$class;
}

# -----------------------------------------------------------------------------

=head2 Objektmethoden

=head3 asSeconds() - Dauer in Sekunden

=head4 Synopsis

    $sec = $dur->asSeconds;

=head4 Description

Liefere die Zeitdauer in Sekunden - sofern vorhanden,
mit Nachkommastellen.

=cut

# -----------------------------------------------------------------------------

sub asSeconds {
    my $self = shift;
    return $$self;
}

# -----------------------------------------------------------------------------

=head3 asString() - Dauer als Zeichenkette

=head4 Synopsis

    $str = $dur->asString;
    $str = $dur->asString($prec);

=head4 Description

Liefere die Zeitdauer als Zeichenkette in der Form DdHhMmS.Xs.

=cut

# -----------------------------------------------------------------------------

sub asString {
    my ($self,$prec) = @_;
    return ref($self)->secondsToString($$self,$prec);
}

# -----------------------------------------------------------------------------

=head3 asShortString() - Dauer als kürzestmögliche Zeichenkette

=head4 Synopsis

    $str = $dur->asShortString(@opt);

=head4 Description

Liefere die Zeitdauer als Zeichenkette der Form DdHhMmSs (per
Default, siehe Option -maxUnit), wobei alle Anteile, die 0 sind,
weggelassen werden, sowohl am Anfang als auch am Ende.

=head4 Options

=over 4

=item -maxUnit => 'd'|'h'|'m'|'s' (Default: 'd')

Größte dargestellte Einheit.

=item -minUnit => 'd'|'h'|'m'|'s' (Default: 's')

Kleinste dargestellte Einheit.

=item -notBlank => $bool (Default: 0)

Bei einer Zeit von 0 Sekunden wird ein Leerstring geliefert.
Ist diese Option gesetzt, erfolgt immer eine Ausgabe -
mit -minUnit als Einheit.

=item -precision => $n (Default: 0)

Anzahl der Sekunden-Nachkommastellen.

=back

=cut

# -----------------------------------------------------------------------------

sub asShortString {
    my $self = shift;
    # @_: @opt

    my $maxUnit = 'd';
    my $minUnit = 's';
    my $notBlank = 0;
    my $precision = 0;

    if (@_) {
        Prty::Option->extract(\@_,
            -maxUnit=>\$maxUnit,
            -minUnit=>\$minUnit,
            -notBlank=>\$notBlank,
            -precision=>\$precision,
        );
    }

    my @unit = qw/d h m s/;
    my @arr = $self->asArray;

    if ($maxUnit =~ /^[hms]$/ && $arr[0]) {
        $arr[1] += $arr[0]*24;
        $arr[0] = 0;
    }
    if ($maxUnit =~ /^[ms]$/ && $arr[1]) {
        $arr[2] += $arr[1]*60;
        $arr[1] = 0;
    }
    if ($maxUnit eq 's' && $arr[2]) {
        $arr[3] += $arr[2]*60;
        $arr[2] = 0;
    }

    if ($minUnit =~ /^[dhm]$/ && $arr[3]) {
        $arr[2] += Prty::Math->roundToInt($arr[3]/60);
        $arr[3] = 0;
    }
    if ($minUnit =~ /^[dh]$/ && $arr[2]) {
        $arr[1] += Prty::Math->roundToInt($arr[2]/60);
        $arr[2] = 0;
    }
    if ($minUnit eq 'd' && $arr[1]) {
        $arr[0] += Prty::Math->roundToInt($arr[1]/24);
        $arr[1] = 0;
    }

    if ($precision) {
        $arr[3] = sprintf '%.*f',$precision,$arr[3];
    }

    my $str = '';
    for my $i (0 .. 3) {
        if ($arr[$i] != 0) {
            $str .= $arr[$i].$unit[$i];
        }
    }
    if ($str eq '' && $notBlank) {
        $str = "0$minUnit";
    }

    return $str;
}

# -----------------------------------------------------------------------------

=head3 asArray() - Dauer als Array

=head4 Synopsis

    @arr | $arr = $dur->asArray;

=head4 Description

Liefere die Dauer als Array mit den Komponenten
($days,$hours,$minutes,$seconds).

=cut

# -----------------------------------------------------------------------------

sub asArray {
    my $self = shift;

    my @arr;
    my $sec = $$self;
    for my $x (86400,3600,60) {
        my $fac = int($sec/$x);
        push @arr,$fac;
        $sec -= $fac*$x;
    }
    push @arr,$sec;

    return wantarray? @arr: \@arr;
}

# -----------------------------------------------------------------------------

=head3 asFFmpegString() - Dauer als Parameter für ffmpeg-Option -t

=head4 Synopsis

    $str = $dur->asFFmpegString;

=head4 Description

Liefere Dauer in der Form wie sie ffmpeg bei der Option -t erwartet,
also im Format

    H:M:S.XXX

=cut

# -----------------------------------------------------------------------------

sub asFFmpegString {
    my $self = shift;
    my @arr = $self->asArray;
    return sprintf '%d:%d:%.3f',$arr[1],$arr[2],$arr[3];
}

# -----------------------------------------------------------------------------

=head3 stringToSeconds() - Wandele Zeitdauer-Angabe in Sekunden

=head4 Synopsis

    $sec = $this->stringToSeconds($str);

=head4 Description

Wandele Zeichenkette zur Bezeichnung einer Zeitdauer in die Anzahl
Sekunden.

=head4 Example

Zeitdauer-Zeichenkette bestehend aus Tagen, Stunden, Mintuten, Sekunden:

    $sec = Prty::Duration->stringToSeconds('152d5h25m3.457s');
    # 13152303.457

=cut

# -----------------------------------------------------------------------------

sub stringToSeconds {
    my ($class,$str) = @_;

    my $sec = 0;
    my @arr = split /([dhms])/,$str;
    for (my $i = 0; $i < @arr; $i += 2) {
        my $x = $arr[$i];
        my $unit = $arr[$i+1] || 's';
        if ($unit eq 'd') {
            $sec += $x*86400;
        }
        elsif ($unit eq 'h') {
            $sec += $x*3600;
        }
        elsif ($unit eq 'm') {
            $sec += $x*60;
        }
        elsif ($unit eq 's') {
            $sec += $x;
        }
        else {
            $class->throw(
                q{DURATION-00001: Unbekannter Anteil einer Dauer},
                Unit=>$unit,
                Value=>$x,
            );
        }
    }

    return $sec;
}

# -----------------------------------------------------------------------------

=head3 secondsToString() - Wandele Sekunden in Zeitdauer-Zeichenkette

=head4 Synopsis

    $str = $this->secondsToString($sec,@opt);

=head4 Description

Wandele Anzahl Sekunden in eine Zeichenkette zur Bezeichnung einer
Zeitdauer.

=head4 Arguments

=over 4

=item $sec

Anzahl Sekunden, ggf. mit Nachkommastellen.

=back

=head4 Options

=over 4

=item $prec (Default: 0)

Anzahl der Nachkommastellen bei den Sekunden. Ist kein Wert angegeben,
wird auf ganze Sekunden gerundet.

=item $unit (Default: undef)

Liefere String fester Breite ab Einheit $unit.

=back

=cut

# -----------------------------------------------------------------------------

sub secondsToString {
    my $class = shift;
    my $sec = shift;
    # @_: $prec -and/or- $unit (Reihenfolge egal)

    # Negative Sekundenzahl

    my $minusSign = 0;
    if ($sec < 0) {
        $sec = abs $sec;
        $minusSign = 1;
    }

    # Optionen

    my $unit = '';
    my $prec = 0;
    while (@_) {
        if (!defined $_[0]) {
            # undef übergehen wir
            shift;
        }
        elsif ($_[0] =~ /^\d+$/) {
            $prec = shift;
        }
        else {
            $unit = shift;
        }
    }

    # Operation ausführen

    my @unit = qw/d h m s/;
    my @arr = $class->new($sec)->asArray;

    my $str;
    my $append = 0;
    for (my $i = 0; $i <= 2; $i++) {
        if ($arr[$i] || $unit eq $unit[$i]) {
            $append = 1;
        }
        if ($append) {
            if ($unit) {
                $str .= sprintf '%02d%s',$arr[$i],$unit[$i];
            }
            else {
                $str .= sprintf '%d%s',$arr[$i],$unit[$i];
            }
        }
    }

    # Sekundenanteil immer liefern

    if ($unit) {
        $str .= sprintf "%02.*f%s",$prec,$arr[3],$unit[3];
        # $str =~ s/^0//; # etwaig führende 0 entfernen 
    }
    else {
        $str .= sprintf "%.*f%s",$prec,$arr[3],$unit[3];
        # 0-Angaben vom Ende her entfernen
        # FIXME: nicht nur Sekunden, sondern auch Minuten etc. entfernen,
        # wenn sie 0 sind. Besseren Ansatz wählen.
        # $str =~ s/(\D)0s$/$1/;
    }

    return $minusSign? "-$str": $str;
}

# -----------------------------------------------------------------------------

=head1 VERSION

1.111

=head1 AUTHOR

Frank Seitz, L<http://fseitz.de/>

=head1 COPYRIGHT

Copyright (C) 2017 Frank Seitz

=head1 LICENSE

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

=cut

# -----------------------------------------------------------------------------

1;

# eof
