jeecgboot/JeecgBoot · error · ParseException

Offset from last day must be <= {MAX_LAST_DAY_OFFSET}

Error message

Offset from last day must be <= {MAX_LAST_DAY_OFFSET}

What it means

For DAY_OF_MONTH the 'L-N' syntax means 'N days before the last day of month' (e.g. L-3 = third-to-last day). The parser caps the offset at MAX_LAST_DAY_OFFSET (=30); an offset greater than 30 is rejected because no month has more than 31 days so a larger backward offset is meaningless. The thrown message interpolates the constant so it reads '<= 30'.

Source

Thrown at jeecg-boot/jeecg-server-cloud/jeecg-visual/jeecg-cloud-xxljob/src/main/java/com/xxl/job/admin/business/scheduler/cron/CronExpression.java:685

        } else if (c == 'L') {

            if(type < DAY_OF_MONTH)
                throw new ParseException("'L' not expected in seconds, minutes or hours fields.", i);

            i++;
            if (type == DAY_OF_WEEK) {
                addToSet(7, 7, 0, type);
            }
            if (type == DAY_OF_MONTH) {
                int dom = LAST_DAY_OFFSET_END;
                boolean nearestWeekday = false;
                if (s.length() > i) {
                    c = s.charAt(i);
                    if (c == '-') {
                        ValueSet vs = getValue(0, s, i + 1);
                        int offset = vs.value;
                        if (offset > MAX_LAST_DAY_OFFSET)
                            throw new ParseException("Offset from last day must be <= " + MAX_LAST_DAY_OFFSET, i + 1);
                        dom -= offset;
                        i = vs.pos;
                    }
                    if (s.length() > i) {
                        c = s.charAt(i);
                        if (c == 'W') {
                            nearestWeekday = true;
                            i++;
                        }
                    }
                }
                if (nearestWeekday) {
                    nearestWeekdays.add(dom);
                } else {
                    daysOfMonth.add(dom);
                }
            }
            return i;

View on GitHub (pinned to 96fb33f5ec)

Solutions

  1. Keep the L-offset between 0 and 30 (L-0 or just L = last day).
  2. If you need an earlier date, use an explicit day number (e.g. '15') instead of a large L-offset.
  3. Clamp computed offsets to the 0-30 range before assembling the cron string.

Example fix

// before - offset beyond the 30-day cap
String cron = "0 0 0 L-40 * ?";  // Offset from last day must be <= 30

// after - valid offset, or explicit day
String cron = "0 0 0 L-5 * ?";
// or a concrete day number
String cron = "0 0 0 1 * ?";
Defensive patterns

Strategy: validation

Validate before calling

private static final int MAX_LAST_DAY_OFFSET = 30;

// Validate an 'L-N' day-of-month token before assembling the cron string.
public static String validateLastDayOffset(String domField) {
    if (domField == null || !domField.startsWith("L-")) return null;
    try {
        int offset = Integer.parseInt(domField.substring(2));
        if (offset < 0 || offset > MAX_LAST_DAY_OFFSET) {
            return "L-offset must be 0.." + MAX_LAST_DAY_OFFSET + ", got " + offset;
        }
    } catch (NumberFormatException ex) {
        return "L-offset must be an integer: " + domField;
    }
    return null;
}

Type guard

public static boolean isValidLastDayOffsetToken(String token) {
    if (token == null) return false;
    if (token.equals("L") || token.matches("LW?") || token.matches("LW")) return true;
    java.util.regex.Matcher m = java.util.regex.Pattern.compile("^L-(\\d+)W?$").matcher(token);
    if (!m.matches()) return false;
    int off = Integer.parseInt(m.group(1));
    return off >= 0 && off <= 30;
}

Try / catch

try {
    new CronExpression(expr);
} catch (ParseException e) {
    if (e.getMessage().startsWith("Offset from last day")) {
        return "The L-offset must be <= 30. Use an explicit day number for earlier dates. " + e.getMessage();
    }
    throw e;
}

Prevention

When it happens

Trigger: A day-of-month like 'L-31', 'L-40', or a computed 'L-' + largeNumber. Also negative or garbage that parseInt turns into a large value. Index i+1 points at the digit start.

Common situations: Programmatic offset that is unbounded; confusion about whether the offset is 0- or 1-based; trying to express 'end of quarter' with a fixed large offset.

Related errors


AI-assisted analysis of jeecgboot/JeecgBoot@96fb33f5ec (2026-08-14). Data as JSON: /api/errors/9b703f9b87a1b1b3. Report an issue: GitHub.