001/*
002 * ============================================================================
003 * Copyright © 2002-2023 by Thomas Thrien.
004 * All Rights Reserved.
005 * ============================================================================
006 * Licensed to the public under the agreements of the GNU Lesser General Public
007 * License, version 3.0 (the "License"). You may obtain a copy of the License at
008 *
009 *      http://www.gnu.org/licenses/lgpl.html
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
013 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
014 * License for the specific language governing permissions and limitations
015 * under the License.
016 */
017
018package org.tquadrat.foundation.value;
019
020import static java.lang.String.format;
021import static java.util.FormattableFlags.ALTERNATE;
022import static java.util.FormattableFlags.LEFT_JUSTIFY;
023import static org.apiguardian.api.API.Status.STABLE;
024import static org.tquadrat.foundation.lang.Objects.isNull;
025import static org.tquadrat.foundation.lang.Objects.requireNonNullArgument;
026import static org.tquadrat.foundation.util.StringUtils.padLeft;
027import static org.tquadrat.foundation.util.StringUtils.padRight;
028import static org.tquadrat.foundation.value.Time.DAY;
029import static org.tquadrat.foundation.value.Time.HOUR;
030import static org.tquadrat.foundation.value.Time.MINUTE;
031import static org.tquadrat.foundation.value.Time.NANOSECOND;
032import static org.tquadrat.foundation.value.Time.WEEK;
033import static org.tquadrat.foundation.value.Time.YEAR;
034
035import java.io.Serial;
036import java.math.BigDecimal;
037import java.time.Duration;
038import java.time.Period;
039import java.time.temporal.ChronoUnit;
040import java.util.Formatter;
041
042import org.apiguardian.api.API;
043import org.tquadrat.foundation.annotation.ClassVersion;
044import org.tquadrat.foundation.value.api.ValueBase;
045
046/**
047 *  A value class for <i>times</i>, what means <i>periods of time</i> in this
048 *  case, opposite to the time displayed on the wall-clock.
049 *
050 *  @extauthor Thomas Thrien - thomas.thrien@tquadrat.org
051 *  @version $Id: TimeValue.java 1073 2023-10-01 11:08:51Z tquadrat $
052 *  @since 0.1.0
053 *
054 *  @UMLGraph.link
055 */
056@ClassVersion( sourceVersion = "$Id: TimeValue.java 1073 2023-10-01 11:08:51Z tquadrat $" )
057@API( status = STABLE, since = "0.1.0" )
058public final class TimeValue extends ValueBase<Time,TimeValue>
059{
060        /*------------------------*\
061    ====** Static Initialisations **===========================================
062        \*------------------------*/
063    /**
064     *  The serial version UID for objects of this class: {@value}.
065     *
066     *  @hidden
067     */
068    @Serial
069    private static final long serialVersionUID = 1729884766468723788L;
070
071        /*--------------*\
072    ====** Constructors **=====================================================
073        \*--------------*/
074    /**
075     *  Creates a new {@code TimeValue} instance.
076     *
077     *  @param  dimension   The dimension.
078     *  @param  value   The value.
079     */
080    public TimeValue( final Time dimension, final BigDecimal value )
081    {
082        //noinspection unchecked
083        super( dimension, value, DEFAULT_VALIDATOR );
084    }   //  TimeValue()
085
086    /**
087     *  Creates a new {@code TimeValue} instance.
088     *
089     *  @param  dimension   The dimension.
090     *  @param  value   The value; it must be possible to parse the given
091     *      String into a
092     *      {@link BigDecimal}.
093     *  @throws NumberFormatException   The provided value cannot be converted
094     *      into a {@code BigDecimal}.
095     */
096    public TimeValue( final Time dimension, final String value ) throws NumberFormatException
097    {
098        //noinspection unchecked
099        super( dimension, value, DEFAULT_VALIDATOR );
100    }   //  TimeValue()
101
102    /**
103     *  Creates a new {@code TimeValue} instance.
104     *
105     *  @param  <N> The type of {@code value}.
106     *  @param  dimension   The dimension.
107     *  @param  value   The value.
108     */
109    public <N extends Number> TimeValue( final Time dimension, final N value )
110    {
111        //noinspection unchecked
112        super( dimension, value, DEFAULT_VALIDATOR );
113    }   //  TimeValue()
114
115    /**
116     *  Creates a new {@code TimeValue} instance.
117     *
118     *  @param  dimension   The dimension.
119     *  @param  value   The value.
120     */
121    public TimeValue( final Time dimension, final Duration value )
122    {
123        //noinspection unchecked
124        super( NANOSECOND, convertDurationToNanos( value ), DEFAULT_VALIDATOR );
125        setUnit( dimension );
126    }   //  TimeValue()
127
128    /**
129     *  Creates a new {@code TimeValue} instance.
130     *
131     *  @param  dimension   The dimension.
132     *  @param  value   The value.
133     */
134    public TimeValue( final Time dimension, final Period value )
135    {
136        this( dimension, translatePeriodToDuration( value ) );
137    }   //  TimeValue()
138
139        /*---------*\
140    ====** Methods **==========================================================
141        \*---------*/
142    /**
143     *  Returns this instance of {@code TimeValue} as an instance of
144     *  {@link Duration}.
145     *
146     *  @return The duration that corresponds to this time value.
147     */
148    public final Duration asDuration()
149    {
150        final var billion = new BigDecimal( "1E+9" );
151        final var totalNanos = convert( NANOSECOND );
152        final var seconds = totalNanos.divideToIntegralValue( billion )
153            .longValue();
154        final var remainingNanos = totalNanos.remainder( billion )
155            .longValue();
156        final var retValue = Duration.ofSeconds( seconds, remainingNanos );
157
158        //---* Done *----------------------------------------------------------
159        return retValue;
160    }   //  asDuration()
161
162    /**
163     *  {@inheritDoc}
164     */
165    @Override
166    public final TimeValue clone()
167    {
168        final var retValue = (TimeValue) super.clone();
169
170        //---* Done *----------------------------------------------------------
171        return retValue;
172    }   //  clone()
173
174    /**
175     *  Converts the given instance of
176     *  {@link Duration}
177     *  to nanoseconds.
178     *
179     *  @param  duration    The duration.
180     *  @return The nanoseconds value,
181     */
182    private static final BigDecimal convertDurationToNanos( final Duration duration )
183    {
184        final var retValue = new BigDecimal( requireNonNullArgument( duration, "duration" ).getSeconds() )
185            .multiply( new BigDecimal( "1E+9" ) )
186            .add( new BigDecimal( duration.getNano() ) );
187
188        //---* Done *----------------------------------------------------------
189        return retValue;
190    }   //  convertDurationToNanos()
191
192    /**
193     *  {@inheritDoc}
194     */
195    @SuppressWarnings( "ProhibitedExceptionThrown" )
196    @Override
197    public final void formatTo( final Formatter formatter, final int flags, final int width, final int precision )
198    {
199        if( isNull( formatter ) ) throw new NullPointerException( "formatter is null" );
200
201        if( (flags & ALTERNATE) == ALTERNATE )
202        {
203            final var leftJustified = (flags & LEFT_JUSTIFY) == LEFT_JUSTIFY;
204
205            final var buffer = new StringBuilder();
206
207            final var times = new Time[] {YEAR, WEEK, DAY, HOUR, MINUTE};
208            var result = new BigDecimal [2];
209            result [1] = baseValue();
210            for( final var t : times )
211            {
212                result = result [1].divideAndRemainder( t.factor(), MATH_CONTEXT );
213                if( result [0].longValue() > 0 )
214                {
215                    buffer.append( result [0].longValue() ).append( t.unitSymbol() ).append( ' ' );
216                }
217            }
218            buffer.append( format( "%1.3fs", result [1] ) );
219
220            final var string = width < buffer.length() ? buffer.toString() : leftJustified ? padRight( buffer, width ) : padLeft( buffer, width );
221
222            /*
223             * We do not use Formatter.out().append() because we do not know how to
224             * handle the IOException that could be thrown from
225             * Appendable.append(). Using Formatter.format() assumes that Formatter
226             * knows ...
227             */
228            formatter.format( "%s", string );
229        }
230        else
231        {
232            super.formatTo( formatter, flags, width, precision );
233        }
234    }   //  formatTo()
235
236    /**
237     *  Translates a
238     *  {@link Period}
239     *  instance to a
240     *  {@link Duration}
241     *  instance.
242     *
243     *  @param  period  The period to translate.
244     *  @return The resulting duration.
245     */
246    private static final Duration translatePeriodToDuration( final Period period )
247    {
248        final var months = requireNonNullArgument( period, "period" ).toTotalMonths();
249        final var days = months * ChronoUnit.MONTHS.getDuration().getSeconds() / ChronoUnit.DAYS.getDuration().getSeconds() + (long) period.getDays();
250        final var retValue = Duration.ofDays( days );
251
252        //---* Done *----------------------------------------------------------
253        return retValue;
254    }   //  translatePeriodToDuration()
255}
256//  class TimeValue
257
258/*
259 *  End of File
260 */