Repository navigation
TimeTrigger SLE project feature: contribute ISOTemporals + SIUnit utils #389
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
nguetava
wants to merge
3
commits into
MontiCore:dev
Choose a base branch
from
nguetava:timetrigger-isotemporals-patches
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
66 changes: 66 additions & 0 deletions
66
monticore-grammar/src/main/grammars/de/monticore/temporal/TimeTriggerConditions.mc4
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| /* (c) https://github.com/MontiCore/monticore */ | ||
| package de.monticore.temporal; | ||
|
|
||
| /* Alpha-version: This is intended to become a MontiCore stable grammar. */ | ||
|
|
||
| /** | ||
| * This grammar defines time trigger conditions (such as 'after', 'on', 'every', 'cron') | ||
| * using SI unit literals and ISO 8601 temporals. | ||
| */ | ||
| component grammar TimeTriggerConditions extends de.monticore.siunit.SIUnitLiterals, | ||
| de.monticore.temporal.ISOTemporals { | ||
|
|
||
| interface TimeTriggerCondition; | ||
|
|
||
| AfterCondition implements TimeTriggerCondition = | ||
| "after" (duration:SIUnitLiteral | period:ISOPeriod); | ||
|
|
||
| OnCondition implements TimeTriggerCondition = | ||
| "on" (isoDateTime:ISODateTime | dateOnly:CalendarDate | timeOnly:ISOTime); | ||
|
|
||
| EveryTimeCondition implements TimeTriggerCondition = | ||
| "every" period:SIUnitLiteral (times:NatLiteral "times")? | ||
| | "from" start:ISODateTime "every" period:SIUnitLiteral | ||
| (times:NatLiteral "times")?; | ||
|
|
||
| CronCondition implements TimeTriggerCondition = | ||
| "cron" "[" expression:CronExpression "]"; | ||
|
|
||
| CronExpression = | ||
| minute:CronField | ||
| hour:CronField | ||
| dayOfMonth:CronField | ||
| month:CronField | ||
| dayOfWeek:CronField; | ||
|
|
||
| CronField = | ||
| elements:(CronElement || ",")+; | ||
|
|
||
| interface CronElement; | ||
|
|
||
| CronValue implements CronElement = | ||
| value:CronAtom ("/" step:NatLiteral)?; | ||
|
|
||
| CronRange implements CronElement = | ||
| start:CronAtom "-" end:CronAtom ("/" step:NatLiteral)?; | ||
|
|
||
| interface CronAtom; | ||
|
|
||
| CronWildcard implements CronAtom = "*"; | ||
|
|
||
| CronNumber implements CronAtom = value:NatLiteral; | ||
|
|
||
| CronName implements CronAtom = value:Name; | ||
|
|
||
| // In this composition, date-time values must contain the ISO 'T' designator. | ||
| // This keeps a negative time-only offset such as 12:00:00-05:30 from being | ||
| // tokenized as an ISODateTimeToken suffix. | ||
| fragment token TimeTriggerISODateTimeTime = | ||
| ('T' Digits | 'T' Digits ':' Digits (':' Digits)?) | ||
| FractionToken? TimeShiftToken?; | ||
|
|
||
| @Override | ||
| token ISODateTimeToken = | ||
| SignToken? Digits '-'? 'W'? Digits ('-' Digits)? TimeTriggerISODateTimeTime; | ||
|
|
||
| } | ||
104 changes: 104 additions & 0 deletions
104
monticore-grammar/src/main/java/de/monticore/siunit/util/SIUnitLiteralSupport.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| /* (c) https://github.com/MontiCore/monticore */ | ||
| package de.monticore.siunit.util; | ||
|
|
||
| import de.monticore.literals.mccommonliterals._ast.ASTBasicDoubleLiteral; | ||
| import de.monticore.literals.mccommonliterals._ast.ASTBasicFloatLiteral; | ||
| import de.monticore.literals.mccommonliterals._ast.ASTBasicLongLiteral; | ||
| import de.monticore.literals.mccommonliterals._ast.ASTNatLiteral; | ||
| import de.monticore.literals.mccommonliterals._ast.ASTNumericLiteral; | ||
| import de.monticore.siunit.siunits._ast.ASTSIUnit; | ||
| import de.monticore.siunit.siunits._ast.ASTSIUnitWithPrefix; | ||
| import de.monticore.siunit.siunits._ast.ASTSIUnitWithoutPrefix; | ||
|
|
||
| import java.math.BigDecimal; | ||
| import java.util.Optional; | ||
| import java.util.Set; | ||
|
|
||
| /** | ||
| * Shared utility for extracting numeric values and unit names from | ||
| * {@link de.monticore.siunit.siunitliterals._ast.ASTSIUnitLiteral} nodes. | ||
| * | ||
| * <p>This class is useful for any CoCo that needs to validate SI-unit | ||
| * durations without depending on a specific host language.</p> | ||
| */ | ||
| public final class SIUnitLiteralSupport { | ||
|
|
||
| /** | ||
| * The set of simple SI-unit spellings that represent time durations: | ||
| * milliseconds, seconds, minutes, hours, and days. | ||
| */ | ||
| public static final Set<String> ALLOWED_TIME_UNITS = Set.of("ms", "s", "min", "h", "d"); | ||
|
|
||
| private SIUnitLiteralSupport() { | ||
| } | ||
|
|
||
| /** | ||
| * Safely extracts the numeric value of a literal without propagating | ||
| * {@link NumberFormatException} on overflow or malformed input. | ||
| * | ||
| * @param literal the numeric literal to extract from | ||
| * @return the value as a {@link BigDecimal}, or empty if it cannot be represented | ||
| */ | ||
| public static Optional<BigDecimal> numericValue(ASTNumericLiteral literal) { | ||
| try { | ||
| if (literal instanceof ASTNatLiteral v) { | ||
| return Optional.of(BigDecimal.valueOf(v.getValue())); | ||
| } | ||
| if (literal instanceof ASTBasicLongLiteral v) { | ||
| return Optional.of(BigDecimal.valueOf(v.getValue())); | ||
| } | ||
| if (literal instanceof ASTBasicFloatLiteral v) { | ||
| return Optional.of(BigDecimal.valueOf(v.getValue())); | ||
| } | ||
| if (literal instanceof ASTBasicDoubleLiteral v) { | ||
| return Optional.of(BigDecimal.valueOf(v.getValue())); | ||
| } | ||
| } | ||
| catch (NumberFormatException exception) { | ||
| return Optional.empty(); | ||
| } | ||
| return Optional.empty(); | ||
| } | ||
|
|
||
| /** | ||
| * Extracts the name of a simple (non-compound) SI unit. | ||
| * | ||
| * @param unit the SI unit AST node | ||
| * @return the unit name (e.g. {@code "ms"}, {@code "s"}, {@code "min"}), | ||
| * or empty for compound or unrecognised units | ||
| */ | ||
| public static Optional<String> simpleUnit(ASTSIUnit unit) { | ||
| if (!unit.isPresentSIUnitPrimitive()) { | ||
| return Optional.empty(); | ||
| } | ||
| var primitive = unit.getSIUnitPrimitive(); | ||
| if (primitive.isPresentSIUnitWithPrefix()) { | ||
| return unitName(primitive.getSIUnitWithPrefix()); | ||
| } | ||
| if (primitive.isPresentSIUnitWithoutPrefix()) { | ||
| return unitName(primitive.getSIUnitWithoutPrefix()); | ||
| } | ||
| return Optional.empty(); | ||
| } | ||
|
|
||
| private static Optional<String> unitName(ASTSIUnitWithPrefix unit) { | ||
| if (unit.isPresentName()) { | ||
| return Optional.of(unit.getName()); | ||
| } | ||
| if (unit.isPresentNonNameUnit()) { | ||
| return Optional.of(unit.getNonNameUnit()); | ||
| } | ||
| return Optional.empty(); | ||
| } | ||
|
|
||
| private static Optional<String> unitName(ASTSIUnitWithoutPrefix unit) { | ||
| if (unit.isPresentName()) { | ||
| return Optional.of(unit.getName()); | ||
| } | ||
| if (unit.isPresentNonNameUnit()) { | ||
| return Optional.of(unit.getNonNameUnit()); | ||
| } | ||
| return Optional.empty(); | ||
| } | ||
|
|
||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
79 changes: 79 additions & 0 deletions
79
...rammar/src/main/java/de/monticore/temporal/isotemporals/util/ISOTemporalsConversions.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| /* (c) https://github.com/MontiCore/monticore */ | ||
| package de.monticore.temporal.isotemporals.util; | ||
|
|
||
| import de.monticore.temporal.isotemporals._ast.ASTCalendarDate; | ||
| import de.monticore.temporal.isotemporals._ast.ASTISODateTime; | ||
| import de.monticore.temporal.isotemporals._ast.ASTISOTime; | ||
|
|
||
| import java.time.LocalDate; | ||
| import java.time.OffsetDateTime; | ||
| import java.time.OffsetTime; | ||
| import java.time.format.DateTimeFormatter; | ||
| import java.util.Objects; | ||
|
|
||
| /** | ||
| * Java-time conversion helpers for {@link de.monticore.temporal.isotemporals._ast} | ||
| * AST nodes. | ||
| * | ||
| * <p>All methods require an explicit UTC offset in the source value. | ||
| * Named time-zone identifiers are not supported; only {@code Z} and numeric | ||
| * offsets such as {@code +02:00} or {@code -05:30} are accepted.</p> | ||
| */ | ||
| public final class ISOTemporalsConversions { | ||
|
|
||
| private ISOTemporalsConversions() { | ||
| } | ||
|
|
||
| /** | ||
| * Converts an ISO 8601 date-time node to an {@link OffsetDateTime}. | ||
| * | ||
| * <p>Hour and minute components of the UTC offset are preserved | ||
| * separately, so offsets such as {@code +05:45} and {@code -00:30} | ||
| * are represented correctly.</p> | ||
| * | ||
| * @param value the parsed ISO date-time node (must not be {@code null}) | ||
| * @return the corresponding {@link OffsetDateTime} | ||
| * @throws java.time.format.DateTimeParseException if the value cannot be parsed | ||
| */ | ||
| public static OffsetDateTime toOffsetDateTime(ASTISODateTime value) { | ||
| Objects.requireNonNull(value, "value"); | ||
| return OffsetDateTime.parse(value.toRawString(), DateTimeFormatter.ISO_OFFSET_DATE_TIME); | ||
| } | ||
|
|
||
| /** | ||
| * Converts an ISO 8601 calendar-date node to a {@link LocalDate}. | ||
| * | ||
| * <p>Date-only values carry no time-zone information. Callers that need | ||
| * an instant must supply a time zone and time-of-day themselves.</p> | ||
| * | ||
| * @param value the parsed ISO calendar-date node (must not be {@code null}) | ||
| * @return the corresponding {@link LocalDate} | ||
| * @throws java.time.format.DateTimeParseException if the value cannot be parsed | ||
| */ | ||
| public static LocalDate toLocalDate(ASTCalendarDate value) { | ||
| Objects.requireNonNull(value, "value"); | ||
| return LocalDate.parse(value.toRawString(), DateTimeFormatter.ISO_LOCAL_DATE); | ||
| } | ||
|
|
||
| /** | ||
| * Converts an ISO 8601 time node to an {@link OffsetTime}. | ||
| * | ||
| * <p>An explicit UTC offset is required. The optional leading {@code T} | ||
| * designator is stripped before parsing so that both {@code T12:00:00Z} | ||
| * and {@code 12:00:00Z} are accepted.</p> | ||
| * | ||
| * @param value the parsed ISO time node (must not be {@code null}) | ||
| * @return the corresponding {@link OffsetTime} | ||
| * @throws java.time.format.DateTimeParseException if the value cannot be parsed | ||
| * or lacks a UTC offset | ||
| */ | ||
| public static OffsetTime toOffsetTime(ASTISOTime value) { | ||
| Objects.requireNonNull(value, "value"); | ||
| String source = value.toRawString(); | ||
| if (source.startsWith("T")) { | ||
| source = source.substring(1); | ||
| } | ||
| return OffsetTime.parse(source, DateTimeFormatter.ISO_OFFSET_TIME); | ||
| } | ||
|
|
||
| } |
58 changes: 58 additions & 0 deletions
58
...va/de/monticore/temporal/timetriggerconditions/_cocos/AfterConditionHasValidDuration.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| /* (c) https://github.com/MontiCore/monticore */ | ||
| package de.monticore.temporal.timetriggerconditions._cocos; | ||
|
|
||
| import de.monticore.siunit.siunitliterals._ast.ASTSIUnitLiteral; | ||
| import de.monticore.temporal.timetriggerconditions._ast.ASTAfterCondition; | ||
| import de.se_rwth.commons.logging.Log; | ||
|
|
||
| import java.math.BigDecimal; | ||
| import java.util.Optional; | ||
| import java.util.Set; | ||
|
|
||
| import static de.monticore.siunit.util.SIUnitLiteralSupport.ALLOWED_TIME_UNITS; | ||
| import static de.monticore.siunit.util.SIUnitLiteralSupport.numericValue; | ||
| import static de.monticore.siunit.util.SIUnitLiteralSupport.simpleUnit; | ||
|
|
||
| /** | ||
| * Checks that an after timer uses a positive duration and a time unit. | ||
| */ | ||
| public class AfterConditionHasValidDuration implements | ||
| TimeTriggerConditionsASTAfterConditionCoCo { | ||
|
|
||
| /** Error code for a zero or otherwise non-positive duration. */ | ||
| public static final String NON_POSITIVE_DURATION = "0xF0001"; | ||
| /** Error code for a non-time, compound, or unsupported unit. */ | ||
| public static final String UNSUPPORTED_UNIT = "0xF0002"; | ||
| /** Error code for a numeric literal that cannot be represented. */ | ||
| public static final String INVALID_NUMERIC_LITERAL = "0xF0003"; | ||
| /** The complete set of simple SI-unit spellings supported by {@code after}. */ | ||
| public static final Set<String> ALLOWED_UNITS = ALLOWED_TIME_UNITS; | ||
|
|
||
| /** Creates the stateless duration CoCo. */ | ||
| public AfterConditionHasValidDuration() { | ||
| } | ||
|
|
||
| @Override | ||
| public void check(ASTAfterCondition node) { | ||
| if (!node.isPresentDuration()) { | ||
| return; | ||
| } | ||
| ASTSIUnitLiteral duration = node.getDuration(); | ||
| Optional<BigDecimal> value = numericValue(duration.getNumericLiteral()); | ||
| Optional<String> unit = simpleUnit(duration.getSIUnit()); | ||
|
|
||
| if (value.isEmpty()) { | ||
| Log.error(INVALID_NUMERIC_LITERAL + " Invalid numeric value in after timer.", node | ||
| .get_SourcePositionStart()); | ||
| } | ||
| else if (value.get().signum() <= 0) { | ||
| Log.error(NON_POSITIVE_DURATION + " After timer duration must be greater than 0.", node | ||
| .get_SourcePositionStart()); | ||
| } | ||
| if (unit.isEmpty() || !ALLOWED_UNITS.contains(unit.get())) { | ||
| Log.error(UNSUPPORTED_UNIT + " After timer unit must be one of ms, s, min, h, or d.", node | ||
| .get_SourcePositionStart()); | ||
| } | ||
| } | ||
|
|
||
| } |
56 changes: 56 additions & 0 deletions
56
...java/de/monticore/temporal/timetriggerconditions/_cocos/AfterConditionHasValidPeriod.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| /* (c) https://github.com/MontiCore/monticore */ | ||
| package de.monticore.temporal.timetriggerconditions._cocos; | ||
|
|
||
| import de.monticore.temporal.isotemporals._ast.ASTFullPeriod; | ||
| import de.monticore.temporal.isotemporals._ast.ASTISOPeriod; | ||
| import de.monticore.temporal.isotemporals._ast.ASTWeekPeriod; | ||
| import de.monticore.temporal.timetriggerconditions._ast.ASTAfterCondition; | ||
| import de.se_rwth.commons.logging.Log; | ||
|
|
||
| /** | ||
| * Checks that an ISO 8601 after timer represents a positive period. | ||
| */ | ||
| public class AfterConditionHasValidPeriod implements | ||
| TimeTriggerConditionsASTAfterConditionCoCo { | ||
|
|
||
| /** Error code for an ISO period that does not denote a positive duration. */ | ||
| public static final String NON_POSITIVE_PERIOD = "0xF0008"; | ||
|
|
||
| /** Creates the stateless ISO-period CoCo. */ | ||
| public AfterConditionHasValidPeriod() { | ||
| } | ||
|
|
||
| @Override | ||
| public void check(ASTAfterCondition node) { | ||
| if (!node.isPresentPeriod()) { | ||
| return; | ||
| } | ||
| if (!isPositive(node.getPeriod())) { | ||
| Log.error(NON_POSITIVE_PERIOD + " ISO after timer period must be greater than 0.", node | ||
| .get_SourcePositionStart()); | ||
| } | ||
| } | ||
|
|
||
| /** Returns whether at least one period component is positive. */ | ||
| protected boolean isPositive(ASTISOPeriod period) { | ||
| if (period instanceof ASTWeekPeriod weekPeriod) { | ||
| return weekPeriod.getWeeks() > 0; | ||
| } | ||
| if (period instanceof ASTFullPeriod fullPeriod) { | ||
| return (fullPeriod.isPresentYears() && fullPeriod.getYears() > 0) || (fullPeriod | ||
| .isPresentMonths() && fullPeriod.getMonths() > 0) || (fullPeriod.isPresentDays() | ||
| && fullPeriod.getDays() > 0) || (fullPeriod.isPresentHours() && fullPeriod.getHours() | ||
| > 0) || (fullPeriod.isPresentMinutes() && fullPeriod.getMinutes() > 0) | ||
| || (fullPeriod.isPresentSeconds() && fullPeriod.getSeconds() > 0) || hasPositiveFraction( | ||
| fullPeriod); | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| /** Returns whether a parsed fractional component contains a non-zero digit. */ | ||
| protected boolean hasPositiveFraction(ASTFullPeriod period) { | ||
| return period.isPresentDecimalDigits() && period.getDecimalDigits().chars().anyMatch( | ||
| character -> character != '0'); | ||
| } | ||
|
|
||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.