You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@isis.apache.org by ah...@apache.org on 2022/01/04 08:54:50 UTC

[isis] branch master updated: ISIS-2882: introduces TimePrecision for time parsing (applib)

This is an automated email from the ASF dual-hosted git repository.

ahuber pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/isis.git


The following commit(s) were added to refs/heads/master by this push:
     new d993037  ISIS-2882: introduces TimePrecision for time parsing (applib)
d993037 is described below

commit d993037806b2ab93ff71764abc3e3bf10583ca52
Author: Andi Huber <ah...@apache.org>
AuthorDate: Tue Jan 4 09:54:39 2022 +0100

    ISIS-2882: introduces TimePrecision for time parsing (applib)
    
    - while Java's FormatStyle is for rendering customization, the new
    TimePrecision is specific for parsing/editing and allows to specify from
    HOUR up to NANO_SECOND precision
---
 .../isis/applib/annotation/TimePrecision.java      |  59 ++++++++++
 .../isis/applib/annotation/ValueSemantics.java     |  15 ++-
 .../value/semantics/TemporalValueSemantics.java    | 120 +++++++++++++++++++--
 .../value/semantics/ValueSemanticsAbstract.java    |  63 +++--------
 .../temporal/TemporalValueSemanticsProvider.java   |  42 ++++++--
 .../JavaUtilDateValueSemanticsProviderTest.java    |  19 ++--
 .../TemporalValueSemanticsProviderTest.java        | 113 +++++++++++++++++++
 7 files changed, 354 insertions(+), 77 deletions(-)

diff --git a/api/applib/src/main/java/org/apache/isis/applib/annotation/TimePrecision.java b/api/applib/src/main/java/org/apache/isis/applib/annotation/TimePrecision.java
new file mode 100644
index 0000000..1c66f5d
--- /dev/null
+++ b/api/applib/src/main/java/org/apache/isis/applib/annotation/TimePrecision.java
@@ -0,0 +1,59 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.isis.applib.annotation;
+
+/**
+ * Precision for time of day.
+ * @since 2.x {@index}
+ */
+public enum TimePrecision {
+
+    UNSPECIFIED,
+
+    /**
+     * 9 fractional digits for <i>Second</i>
+     */
+    NANO_SECOND,
+
+    /**
+     * 6 fractional digits for <i>Second</i>
+     */
+    MICRO_SECOND,
+
+    /**
+     * 3 fractional digits for <i>Second</i>
+     */
+    MILLI_SECOND,
+
+    /**
+     * <i>Second</i>
+     */
+    SECOND,
+
+    /**
+     * <i>Minute</i>
+     */
+    MINUTE,
+
+    /**
+     * <i>Hour</i>
+     */
+    HOUR;
+
+}
diff --git a/api/applib/src/main/java/org/apache/isis/applib/annotation/ValueSemantics.java b/api/applib/src/main/java/org/apache/isis/applib/annotation/ValueSemantics.java
index 17376e0..c30a8ea2 100644
--- a/api/applib/src/main/java/org/apache/isis/applib/annotation/ValueSemantics.java
+++ b/api/applib/src/main/java/org/apache/isis/applib/annotation/ValueSemantics.java
@@ -86,20 +86,29 @@ public @interface ValueSemantics {
     int minFractionalDigits()
             default 0;
 
-    // -- TEMPORAL FORMAT STYLE
+    // -- TEMPORAL FORMATTING
 
     /**
-     * If associated with a temporal date value, the style of a localized date.
+     * If associated with a temporal date value, the rendering style of a localized date.
      * @see FormatStyle
      */
     FormatStyle dateFormatStyle()
             default FormatStyle.MEDIUM;
 
     /**
-     * If associated with a temporal time value, the style of a localized time.
+     * If associated with a temporal time value, the rendering style of a localized time.
      * @see FormatStyle
      */
     FormatStyle timeFormatStyle()
             default FormatStyle.MEDIUM;
 
+    /**
+     * If associated with a temporal time value, the time of day precision,
+     * used for editing a time field in the UI.<br>
+     * default = {@link TimePrecision#SECOND}
+     * @see TimePrecision
+     */
+    TimePrecision timePrecision()
+            default TimePrecision.SECOND;
+
 }
diff --git a/api/applib/src/main/java/org/apache/isis/applib/value/semantics/TemporalValueSemantics.java b/api/applib/src/main/java/org/apache/isis/applib/value/semantics/TemporalValueSemantics.java
index bf0a285..010cdf3 100644
--- a/api/applib/src/main/java/org/apache/isis/applib/value/semantics/TemporalValueSemantics.java
+++ b/api/applib/src/main/java/org/apache/isis/applib/value/semantics/TemporalValueSemantics.java
@@ -24,6 +24,7 @@ import java.time.temporal.Temporal;
 import javax.validation.constraints.NotEmpty;
 import javax.validation.constraints.NotNull;
 
+import org.apache.isis.applib.annotation.TimePrecision;
 import org.apache.isis.commons.internal.exceptions._Exceptions;
 
 import lombok.Data;
@@ -80,6 +81,23 @@ extends
     TemporalCharacteristic getTemporalCharacteristic();
     OffsetCharacteristic getOffsetCharacteristic();
 
+    static enum EditingFormatDirection {
+
+        /**
+         * Input parsable text.
+         */
+        INPUT,
+
+        /**
+         * Output parsable text.
+         */
+        OUTPUT;
+
+        public boolean isInput() {return this == INPUT;}
+        public boolean isOutput() {return this == OUTPUT;}
+    }
+
+
     @Data
     public static class TemporalEditingPattern {
 
@@ -89,13 +107,63 @@ extends
         @NotNull @NotEmpty
         private String datePattern = "yyyy-MM-dd";
 
+        // -- TIME PATTERNS - SECOND
+
         /**
-         * The locale-independent (canonical) pattern used for editing time in the UI.
+         * The locale-independent (canonical) input pattern used for editing time in the UI.
+         * Yielding {@link TimeFormatPrecision#NANO_SECOND}.
          * <p>
-         * When editing, omitting nano-seconds, seconds or minutes will use zeros instead.
+         * Any missing temporal parts are filled up with zeros to meet the {@link TimeFormatPrecision}.
          */
         @NotNull @NotEmpty
-        private String timePattern = "HH:mm:ss"; //FIXME[ISIS-2882] support omitted parts on input "HH[:mm[:ss[.SSSSSSSSS]]]"
+        private String timePatternNanoSecond = "HH[:mm[:ss][.SSSSSSSSS]]";
+
+        /**
+         * The locale-independent (canonical) input pattern used for editing time in the UI.
+         * Yielding {@link TimeFormatPrecision#MICRO_SECOND}.
+         * <p>
+         * Any missing temporal parts are filled up with zeros to meet the {@link TimeFormatPrecision}.
+         */
+        @NotNull @NotEmpty
+        private String timePatternMicroSecond = "HH[:mm[:ss][.SSSSSS]]";
+
+        /**
+         * The locale-independent (canonical) input pattern used for editing time in the UI.
+         * Yielding {@link TimeFormatPrecision#MILLI_SECOND}.
+         * <p>
+         * Any missing temporal parts are filled up with zeros to meet the {@link TimeFormatPrecision}.
+         */
+        @NotNull @NotEmpty
+        private String timePatternMilliSecond = "HH[:mm[:ss][.SSS]]";
+
+        /**
+         * The locale-independent (canonical) input pattern used for editing time in the UI.
+         * Yielding {@link TimeFormatPrecision#SECOND}.
+         * <p>
+         * Any missing temporal parts are filled up with zeros to meet the {@link TimeFormatPrecision}.
+         */
+        @NotNull @NotEmpty
+        private String timePatternSecond = "HH[:mm[:ss]]";
+
+        /**
+         * The locale-independent (canonical) input pattern used for editing time in the UI.
+         * Yielding {@link TimeFormatPrecision#MINUTE}.
+         * <p>
+         * Any missing temporal parts are filled up with zeros to meet the {@link TimeFormatPrecision}.
+         */
+        @NotNull @NotEmpty
+        private String timePatternMinute = "HH[:mm]";
+
+        /**
+         * The locale-independent (canonical) input pattern used for editing time in the UI.
+         * Yielding {@link TimeFormatPrecision#HOUR}.
+         * <p>
+         * Any missing temporal parts are filled up with zeros to meet the {@link TimeFormatPrecision}.
+         */
+        @NotNull @NotEmpty
+        private String timePatternHour = "HH";
+
+        // -- ZONE PATTERN
 
         /**
          * The locale-independent (canonical) pattern used for editing time-zone in the UI.
@@ -103,6 +171,8 @@ extends
         @NotNull @NotEmpty
         private String zonePattern = "x";
 
+        // -- JOINING PATTERNS
+
         /**
          * The locale-independent (canonical) pattern used for editing date and time in the UI.
          * <p>
@@ -126,12 +196,14 @@ extends
 
         public String getEditingFormatAsPattern(
                 final @NonNull TemporalCharacteristic temporalCharacteristic,
-                final @NonNull OffsetCharacteristic offsetCharacteristic) {
+                final @NonNull OffsetCharacteristic offsetCharacteristic,
+                final @NonNull TimePrecision timePrecision,
+                final @NonNull EditingFormatDirection direction) {
 
             switch (temporalCharacteristic) {
             case DATE_TIME:
                 val dateTimePattern =
-                    String.format(getDateTimeJoiningPattern(), getDatePattern(), getTimePattern());
+                    String.format(getDateTimeJoiningPattern(), getDatePattern(), timePattern(timePrecision, direction));
                 return offsetCharacteristic.isLocal()
                         ? dateTimePattern
                         : String.format(getZoneJoiningPattern(), dateTimePattern, getZonePattern());
@@ -141,13 +213,47 @@ extends
                         : String.format(getZoneJoiningPattern(), getDatePattern(), getZonePattern());
             case TIME_ONLY:
                 return offsetCharacteristic.isLocal()
-                        ? getTimePattern()
-                        : String.format(getZoneJoiningPattern(), getTimePattern(), getZonePattern());
+                        ? timePattern(timePrecision, direction)
+                        : String.format(getZoneJoiningPattern(), timePattern(timePrecision, direction), getZonePattern());
             default:
                 throw _Exceptions.unmatchedCase(temporalCharacteristic);
             }
         }
 
+        // -- HELPER
+
+        private String timePattern(
+                final @NonNull TimePrecision timePrecision,
+                final @NonNull EditingFormatDirection direction) {
+            switch (direction) {
+            case INPUT:
+                return timePattern(timePrecision);
+            case OUTPUT:
+                return timePattern(timePrecision)
+                        .replace("[", "").replace("]", ""); // remove brackets for optional temporal parts
+            }
+            throw _Exceptions.unmatchedCase(direction);
+        }
+
+        private String timePattern(final @NonNull TimePrecision timePrecision) {
+            switch (timePrecision) {
+            case NANO_SECOND:
+                return getTimePatternNanoSecond();
+            case MICRO_SECOND:
+                return getTimePatternMicroSecond();
+            case MILLI_SECOND:
+                return getTimePatternMilliSecond();
+            case UNSPECIFIED:
+            case SECOND:
+                return getTimePatternSecond();
+            case MINUTE:
+                return getTimePatternMinute();
+            case HOUR:
+                return getTimePatternHour();
+            }
+            throw _Exceptions.unmatchedCase(timePrecision);
+        }
+
     }
 
 }
diff --git a/api/applib/src/main/java/org/apache/isis/applib/value/semantics/ValueSemanticsAbstract.java b/api/applib/src/main/java/org/apache/isis/applib/value/semantics/ValueSemanticsAbstract.java
index 3d1fc16..53b7151 100644
--- a/api/applib/src/main/java/org/apache/isis/applib/value/semantics/ValueSemanticsAbstract.java
+++ b/api/applib/src/main/java/org/apache/isis/applib/value/semantics/ValueSemanticsAbstract.java
@@ -33,9 +33,11 @@ import java.util.function.Function;
 
 import org.springframework.lang.Nullable;
 
+import org.apache.isis.applib.annotation.TimePrecision;
 import org.apache.isis.applib.exceptions.recoverable.TextEntryParseException;
 import org.apache.isis.applib.locale.UserLocale;
 import org.apache.isis.applib.services.iactnlayer.InteractionContext;
+import org.apache.isis.applib.value.semantics.TemporalValueSemantics.EditingFormatDirection;
 import org.apache.isis.applib.value.semantics.TemporalValueSemantics.TemporalEditingPattern;
 import org.apache.isis.commons.collections.Can;
 import org.apache.isis.commons.internal.base._Strings;
@@ -107,6 +109,14 @@ implements
         .orElseGet(UserLocale::getDefault);
     }
 
+    protected String render(final T value, final Function<T, String> toString) {
+        return Optional.ofNullable(value)
+                .map(toString)
+                .orElse(NULL_REPRESENTATION);
+    }
+
+    // -- NUMBER FORMATTING/PARSING
+
     /**
      * @param context - nullable in support of JUnit testing
      * @return {@link NumberFormat} the default from from given context's locale
@@ -124,14 +134,6 @@ implements
         return format;
     }
 
-    protected String render(final T value, final Function<T, String> toString) {
-        return Optional.ofNullable(value)
-                .map(toString)
-                .orElse(NULL_REPRESENTATION);
-    }
-
-    // -- NUMBER FORMATTING/PARSING
-
     protected @Nullable BigInteger parseInteger(
             final @Nullable ValueSemanticsProvider.Context context,
             final @Nullable String text) {
@@ -211,55 +213,22 @@ implements
         }
     }
 
-    protected DateTimeFormatter getEditingFormat(
+    protected DateTimeFormatter getTemporalEditingFormat(
             final @Nullable ValueSemanticsProvider.Context context,
             final @NonNull TemporalValueSemantics.TemporalCharacteristic temporalCharacteristic,
             final @NonNull TemporalValueSemantics.OffsetCharacteristic offsetCharacteristic,
+            final @NonNull TimePrecision timePrecision,
+            final @NonNull EditingFormatDirection direction,
             final @NonNull TemporalEditingPattern editingPattern) {
 
-        return getEditingFormatAsBuilder(
-                temporalCharacteristic, offsetCharacteristic, editingPattern)
+        return new DateTimeFormatterBuilder()
+                .appendPattern(editingPattern.getEditingFormatAsPattern(
+                        temporalCharacteristic, offsetCharacteristic, timePrecision, direction))
                 .parseLenient()
                 .parseCaseInsensitive()
                 .toFormatter(getUserLocale(context).getTimeFormatLocale());
     }
 
-    protected DateTimeFormatterBuilder getEditingFormatAsBuilder(
-            final @NonNull TemporalValueSemantics.TemporalCharacteristic temporalCharacteristic,
-            final @NonNull TemporalValueSemantics.OffsetCharacteristic offsetCharacteristic,
-            final @NonNull TemporalEditingPattern editingPattern) {
-
-        return new DateTimeFormatterBuilder()
-            .appendPattern(getEditingFormatAsPattern(
-                    temporalCharacteristic, offsetCharacteristic, editingPattern));
-    }
-
-    protected String getEditingFormatAsPattern(
-            final @NonNull TemporalValueSemantics.TemporalCharacteristic temporalCharacteristic,
-            final @NonNull TemporalValueSemantics.OffsetCharacteristic offsetCharacteristic,
-            final @NonNull TemporalEditingPattern editingPattern) {
-
-        switch (temporalCharacteristic) {
-        case DATE_TIME:
-            val dateTimePattern =
-                String.format(editingPattern.getDateTimeJoiningPattern(), editingPattern.getDatePattern(), editingPattern.getTimePattern());
-            return offsetCharacteristic.isLocal()
-                    ? dateTimePattern
-                    : String.format(editingPattern.getZoneJoiningPattern(), dateTimePattern, editingPattern.getZonePattern());
-        case DATE_ONLY:
-            return offsetCharacteristic.isLocal()
-                    ? editingPattern.getDatePattern()
-                    : String.format(editingPattern.getZoneJoiningPattern(), editingPattern.getDatePattern(), editingPattern.getZonePattern());
-        case TIME_ONLY:
-            return offsetCharacteristic.isLocal()
-                    ? editingPattern.getTimePattern()
-                    : String.format(editingPattern.getZoneJoiningPattern(), editingPattern.getTimePattern(), editingPattern.getZonePattern());
-        default:
-            throw _Exceptions.unmatchedCase(temporalCharacteristic);
-        }
-    }
-
-
     protected DateTimeFormatter getTemporalIsoFormat(
             final @NonNull TemporalValueSemantics.TemporalCharacteristic temporalCharacteristic,
             final @NonNull TemporalValueSemantics.OffsetCharacteristic offsetCharacteristic) {
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/valuesemantics/temporal/TemporalValueSemanticsProvider.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/valuesemantics/temporal/TemporalValueSemanticsProvider.java
index 67db9da..9993ff6 100644
--- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/valuesemantics/temporal/TemporalValueSemanticsProvider.java
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/valuesemantics/temporal/TemporalValueSemanticsProvider.java
@@ -31,6 +31,7 @@ import javax.inject.Inject;
 
 import org.springframework.lang.Nullable;
 
+import org.apache.isis.applib.annotation.TimePrecision;
 import org.apache.isis.applib.exceptions.recoverable.TextEntryParseException;
 import org.apache.isis.applib.value.semantics.EncodingException;
 import org.apache.isis.applib.value.semantics.TemporalValueSemantics;
@@ -154,7 +155,7 @@ implements TemporalValueSemantics<T> {
 
     @Override
     public final String parseableTextRepresentation(final ValueSemanticsProvider.Context context, final T value) {
-        return value==null ? "" : getEditingFormat(context).format(value);
+        return value==null ? "" : getEditingOutputFormat(context).format(value);
     }
 
     @Override
@@ -173,7 +174,7 @@ implements TemporalValueSemantics<T> {
             }
         }
 
-        val format = getEditingFormat(context);
+        val format = getEditingInputFormat(context);
 
         try {
             return format.parse(temporalString, query);
@@ -199,17 +200,38 @@ implements TemporalValueSemantics<T> {
     }
 
     /**
-     * Format used for editing.
+     * Format used for rendering editable text representation.
      */
-    protected DateTimeFormatter getEditingFormat(final ValueSemanticsProvider.Context context) {
-        return getEditingFormat(context, temporalCharacteristic, offsetCharacteristic,
+    protected DateTimeFormatter getEditingOutputFormat(final ValueSemanticsProvider.Context context) {
+
+        val dateAndTimeFormatStyle = DateAndTimeFormatStyle.forContext(mmc, context);
+
+        return getTemporalEditingFormat(context, temporalCharacteristic, offsetCharacteristic,
+                dateAndTimeFormatStyle.getTimePrecision(),
+                EditingFormatDirection.OUTPUT,
+                temporalEditingPattern());
+    }
+
+    /**
+     * Format used for parsing editable text representation.
+     */
+    protected DateTimeFormatter getEditingInputFormat(final ValueSemanticsProvider.Context context) {
+
+        val dateAndTimeFormatStyle = DateAndTimeFormatStyle.forContext(mmc, context);
+
+        return getTemporalEditingFormat(context, temporalCharacteristic, offsetCharacteristic,
+                dateAndTimeFormatStyle.getTimePrecision(),
+                EditingFormatDirection.INPUT,
                 temporalEditingPattern());
     }
 
     @Override
     public String getPattern(final ValueSemanticsProvider.Context context) {
-        return getEditingFormatAsPattern(temporalCharacteristic, offsetCharacteristic,
-                temporalEditingPattern());
+
+        val dateAndTimeFormatStyle = DateAndTimeFormatStyle.forContext(mmc, context);
+
+        return temporalEditingPattern().getEditingFormatAsPattern(temporalCharacteristic, offsetCharacteristic,
+                dateAndTimeFormatStyle.getTimePrecision(), EditingFormatDirection.INPUT);
     }
 
     /**
@@ -225,6 +247,7 @@ implements TemporalValueSemantics<T> {
     static class DateAndTimeFormatStyle {
         @Nullable FormatStyle dateFormatStyle;
         @Nullable FormatStyle timeFormatStyle;
+        @Nullable TimePrecision timePrecision;
 
         static DateAndTimeFormatStyle forContext(
                 final @Nullable MetaModelContext mmc, // nullable .. JUnit support
@@ -247,7 +270,10 @@ implements TemporalValueSemantics<T> {
                     .map(TimeFormatStyleFacet::getTimeFormatStyle)
                     .orElse(FormatStyle.MEDIUM);
 
-            return of(dateFormatStyle, timeFormatStyle);
+            //FIXME[ISIS-2882] honor facets
+            val timePrecision = TimePrecision.SECOND;
+
+            return of(dateFormatStyle, timeFormatStyle, timePrecision);
         }
 
     }
diff --git a/core/metamodel/src/test/java/org/apache/isis/core/metamodel/facets/value/JavaUtilDateValueSemanticsProviderTest.java b/core/metamodel/src/test/java/org/apache/isis/core/metamodel/facets/value/JavaUtilDateValueSemanticsProviderTest.java
index 07bb8ca..2511a16 100644
--- a/core/metamodel/src/test/java/org/apache/isis/core/metamodel/facets/value/JavaUtilDateValueSemanticsProviderTest.java
+++ b/core/metamodel/src/test/java/org/apache/isis/core/metamodel/facets/value/JavaUtilDateValueSemanticsProviderTest.java
@@ -60,7 +60,7 @@ extends ValueSemanticsProviderAbstractTestCase {
     }
 
     @Test
-    public void testInvalidParse() throws Exception {
+    void testInvalidParse() {
         try {
             valueSemantics.parseTextRepresentation(null, "invalid entry");
             fail();
@@ -68,20 +68,15 @@ extends ValueSemanticsProviderAbstractTestCase {
         }
     }
 
-    /**
-     * Something rather bizarre here, that the epoch formats as 01:00 rather
-     * than 00:00. It's obviously because of some sort of timezone issue, but I
-     * don't know where that dependency is coming from.
-     */
     @Test
-    public void testRendering() {
+    void testRendering() {
         val _context = Context.of(null, InteractionContext.builder().locale(UserLocale.valueOf(Locale.ENGLISH)).build());
         assertEquals("Mar 13, 2013, 5:59:03 PM", valueSemantics.simpleTextPresentation(_context , date));
     }
 
     //FIXME[ISIS-2882] support omitted parts on input
     @Test @Disabled
-    public void testParseNoMinutes() throws Exception {
+    void testParseNoMinutes() {
         val _context = Context.of(null, InteractionContext.builder().locale(UserLocale.valueOf(Locale.ENGLISH)).build());
         val parsedDate = valueSemantics.parseTextRepresentation(_context, "2013-03-13 17");
         assertEquals(date.getTime() - 3540_000L - 3000L, parsedDate.getTime());
@@ -89,14 +84,14 @@ extends ValueSemanticsProviderAbstractTestCase {
 
     //FIXME[ISIS-2882] support omitted parts on input
     @Test @Disabled
-    public void testParseNoSeconds() throws Exception {
+    void testParseNoSeconds() {
         val _context = Context.of(null, InteractionContext.builder().locale(UserLocale.valueOf(Locale.ENGLISH)).build());
         val parsedDate = valueSemantics.parseTextRepresentation(_context, "2013-03-13 17:59");
         assertEquals(date.getTime() - 3000L, parsedDate.getTime());
     }
 
     @Test
-    public void testParseSeconds() throws Exception {
+    void testParseSeconds() {
         val _context = Context.of(null, InteractionContext.builder().locale(UserLocale.valueOf(Locale.ENGLISH)).build());
         val parsedDate = valueSemantics.parseTextRepresentation(_context, "2013-03-13 17:59:03");
         assertEquals(date.getTime(), parsedDate.getTime());
@@ -107,7 +102,7 @@ extends ValueSemanticsProviderAbstractTestCase {
      * @see https://stackoverflow.com/questions/30103167/jsr-310-parsing-seconds-fraction-with-variable-length
      */
     @Test @Disabled("cannot find a format pattern that can handle both millis and nanos")
-    public void testParseMillis() throws Exception {
+    void testParseMillis() {
         val _context = Context.of(null, InteractionContext.builder().locale(UserLocale.valueOf(Locale.ENGLISH)).build());
         val parsedDate = valueSemantics.parseTextRepresentation(_context, "2013-03-13 17:59:03.123");
         assertEquals(date.getTime() + 123L, parsedDate.getTime());
@@ -115,7 +110,7 @@ extends ValueSemanticsProviderAbstractTestCase {
 
     //FIXME[ISIS-2882] support omitted parts on input
     @Test @Disabled
-    public void testParseNanos() throws Exception {
+    void testParseNanos() {
         val _context = Context.of(null, InteractionContext.builder().locale(UserLocale.valueOf(Locale.ENGLISH)).build());
         val parsedDate = valueSemantics.parseTextRepresentation(_context, "2013-03-13 17:59:03.123456789");
         assertEquals(date.getTime() + 123L, parsedDate.getTime());
diff --git a/core/metamodel/src/test/java/org/apache/isis/core/metamodel/valuesemantics/temporal/TemporalValueSemanticsProviderTest.java b/core/metamodel/src/test/java/org/apache/isis/core/metamodel/valuesemantics/temporal/TemporalValueSemanticsProviderTest.java
new file mode 100644
index 0000000..1041718
--- /dev/null
+++ b/core/metamodel/src/test/java/org/apache/isis/core/metamodel/valuesemantics/temporal/TemporalValueSemanticsProviderTest.java
@@ -0,0 +1,113 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.isis.core.metamodel.valuesemantics.temporal;
+
+import java.time.Duration;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.Temporal;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import org.apache.isis.applib.annotation.TimePrecision;
+import org.apache.isis.applib.locale.UserLocale;
+import org.apache.isis.applib.value.semantics.TemporalValueSemantics;
+import org.apache.isis.applib.value.semantics.TemporalValueSemantics.EditingFormatDirection;
+import org.apache.isis.applib.value.semantics.TemporalValueSemantics.OffsetCharacteristic;
+import org.apache.isis.applib.value.semantics.TemporalValueSemantics.TemporalCharacteristic;
+import org.apache.isis.applib.value.semantics.TemporalValueSemantics.TemporalEditingPattern;
+import org.apache.isis.applib.value.semantics.ValueSemanticsProvider.Context;
+import org.apache.isis.core.config.IsisConfiguration;
+import org.apache.isis.schema.common.v2.ValueType;
+
+import lombok.NonNull;
+import lombok.val;
+
+class TemporalValueSemanticsProviderTest {
+
+    private TemporalValueSemanticsProvider_forTesting target;
+    private TemporalEditingPattern editingPattern;
+
+    @BeforeEach
+    void setUp() throws Exception {
+        editingPattern = (new IsisConfiguration.ValueTypes.Temporal()).getEditing();
+    }
+
+    @ParameterizedTest
+    @EnumSource(TimePrecision.class)
+    void testTimeFormats(final TimePrecision timePrecision) {
+
+        target = new TemporalValueSemanticsProvider_forTesting(
+                TemporalCharacteristic.TIME_ONLY, OffsetCharacteristic.LOCAL);
+
+        Context context = null;
+        LocalTime localTime = LocalTime.of(13, 12, 45);
+
+        val formatter = target.getTemporalEditingFormat(context ,
+                target.getTemporalCharacteristic(),
+                target.getOffsetCharacteristic(),
+                timePrecision,
+                EditingFormatDirection.OUTPUT,
+                editingPattern);
+
+        val formattedTemporal = formatter.format(localTime);
+        assertNotNull(formattedTemporal);
+
+        System.out.println(formattedTemporal);
+    }
+
+    // -- HELPER
+
+    private static class TemporalValueSemanticsProvider_forTesting
+    extends TemporalValueSemanticsProvider<Temporal> {
+
+        public TemporalValueSemanticsProvider_forTesting(
+                final TemporalCharacteristic temporalCharacteristic,
+                final OffsetCharacteristic offsetCharacteristic) {
+            super(temporalCharacteristic, offsetCharacteristic, 80, 80, null, null);
+        }
+
+        @Override public Class<Temporal> getCorrespondingClass() {
+            return Temporal.class;}
+
+        @Override public ValueType getSchemaValueType() {
+            return ValueType.VOID;}
+
+        @Override protected UserLocale getUserLocale(final Context context) {
+            return super.getUserLocale(context);}
+
+        @Override public Duration epsilon() {
+            return null;}
+
+        @Override public DateTimeFormatter getTemporalEditingFormat(final Context context,
+                @NonNull final TemporalValueSemantics.TemporalCharacteristic temporalCharacteristic,
+                @NonNull final TemporalValueSemantics.OffsetCharacteristic offsetCharacteristic,
+                @NonNull final TimePrecision timePrecision,
+                @NonNull final EditingFormatDirection direction,
+                @NonNull final TemporalEditingPattern editingPattern) {
+            return super.getTemporalEditingFormat(context, temporalCharacteristic, offsetCharacteristic, timePrecision, direction,
+                    editingPattern); }
+
+    }
+
+}