Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
Comment thread
nguetava marked this conversation as resolved.
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;

}
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();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,12 @@ public long getLong(TemporalField field) {
case NANO_OF_SECOND:
return getNanoOfSecond();
case OFFSET_SECONDS:
// Time shift is in hours but result has to be in seconds
return 3600L * getTimeShiftHour() + 60L * getTimeShiftMinute();
long offsetSeconds = 3600L * Math.abs(getTimeShiftHour()) + 60L * getTimeShiftMinute();
if (isPresentTimeShiftSource() && getTimeShiftSource().isPresentSign()
&& getTimeShiftSource().getSign() == ASTSign.MINUS) {
return -offsetSeconds;
}
return offsetSeconds;
}
}
throw new UnsupportedTemporalTypeException(field.toString());
Expand Down
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);
}

}
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());
}
}

}
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');
}

}
Loading
Loading