From ec0a1032fb23bf0667228f5277d2e398c074843c Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Tue, 10 Mar 2026 15:32:35 +0100 Subject: [PATCH] Add Time::Piece and Time::Seconds modules Implements Time::Piece date/time manipulation module with: - TimePiece.java providing XS replacement functions (_strftime, _strptime, _crt_localtime, _crt_gmtime, _mini_mktime, _get_localization) - Time/Piece.pm adapted from CPAN with full date arithmetic support - Time/Seconds.pm for duration constants and calculations Also adds POSIX.java with strftime() and mktime() functions using java.time.format.DateTimeFormatter with manual format code mapping. Includes port-cpan-module skill documenting the porting process. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .cognition/skills/port-cpan-module/SKILL.md | 399 +++++++++ docs/about/changelog.md | 2 +- .../perlonjava/runtime/perlmodule/POSIX.java | 309 +++++++ .../runtime/perlmodule/TimePiece.java | 348 ++++++++ src/main/perl/lib/POSIX.pm | 18 + src/main/perl/lib/Time/Piece.pm | 763 ++++++++++++++++++ src/main/perl/lib/Time/Seconds.pm | 260 ++++++ 7 files changed, 2098 insertions(+), 1 deletion(-) create mode 100644 .cognition/skills/port-cpan-module/SKILL.md create mode 100644 src/main/java/org/perlonjava/runtime/perlmodule/POSIX.java create mode 100644 src/main/java/org/perlonjava/runtime/perlmodule/TimePiece.java create mode 100644 src/main/perl/lib/Time/Piece.pm create mode 100644 src/main/perl/lib/Time/Seconds.pm diff --git a/.cognition/skills/port-cpan-module/SKILL.md b/.cognition/skills/port-cpan-module/SKILL.md new file mode 100644 index 000000000..d89c04581 --- /dev/null +++ b/.cognition/skills/port-cpan-module/SKILL.md @@ -0,0 +1,399 @@ +# Port CPAN Module to PerlOnJava + +This skill guides you through porting a CPAN module with XS/C components to PerlOnJava using Java implementations. + +## When to Use This Skill + +- User asks to add a CPAN module to PerlOnJava +- User asks to port a Perl module with XS code +- User wants to implement Perl module functionality in Java + +## Key Principles + +1. **Reuse as much original code as possible** - Most CPAN modules are 70-90% pure Perl. Only the XS/C portions need Java replacements. Copy the original `.pm` code and adapt minimally. + +2. **Always inspect the XS source** - The `.xs` file reveals exactly what needs Java implementation. Study it to understand the C algorithms, edge cases, and expected behavior. + +3. **Credit original authors** - Always preserve the original AUTHORS and COPYRIGHT sections in the POD. Add a note that this is a PerlOnJava port. + +## Overview + +PerlOnJava supports three types of modules: +1. **Pure Perl modules** - Work directly, no Java needed +2. **Java-implemented modules (XSLoader)** - Replace XS/C with Java +3. **Built-in modules (GlobalContext)** - Internal only + +**Most CPAN ports use type #2 (XSLoader).** + +## Step-by-Step Process + +### Phase 1: Analysis + +1. **Fetch the original module source:** + ``` + https://fastapi.metacpan.org/v1/source/AUTHOR/Module-Version/Module.pm + https://fastapi.metacpan.org/v1/source/AUTHOR/Module-Version/Module.xs + ``` + +2. **Study the XS file thoroughly:** + - Look for `MODULE = ` and `PACKAGE = ` declarations + - Identify each XS function (appears after `void` or return type) + - Read the C code to understand algorithms and edge cases + - Note any platform-specific code (WIN32, etc.) + - Check for copyright notices to preserve + +3. **Identify what needs Java implementation:** + - Functions defined in `.xs` files + - Functions that call C libraries (strftime, crypt, etc.) + - Functions loaded via `XSLoader::load()` + +4. **Identify what can be reused as pure Perl (typically 70-90%):** + - Most accessor methods + - Helper/utility functions + - Overloaded operators + - Import/export logic + - Format translation maps + - Constants and configuration + +5. **Check for dependencies:** + - Other modules the target depends on + - Whether those dependencies exist in PerlOnJava + +6. **Check available Java libraries:** + - Review `pom.xml` and `build.gradle` for already-imported dependencies + - Common libraries already available: Gson, jnr-posix, jnr-ffi, SnakeYAML, etc. + - Consider if a Java library can replace the XS functionality directly + +7. **Check existing PerlOnJava infrastructure:** + - `org.perlonjava.runtime.nativ.PosixLibrary` - JNR-POSIX wrapper for native calls + - `org.perlonjava.runtime.nativ.NativeUtils` - Cross-platform utilities with Windows fallbacks + - `org.perlonjava.runtime.operators.*` - Existing operator implementations + +### Phase 2: Create Java Implementation + +**File location:** `src/main/java/org/perlonjava/runtime/perlmodule/` + +**Naming convention:** `Module::Name` → `ModuleName.java` +- `Time::Piece` → `TimePiece.java` +- `Digest::MD5` → `DigestMD5.java` +- `DBI` → `DBI.java` + +**Basic structure:** +```java +package org.perlonjava.runtime.perlmodule; + +import org.perlonjava.runtime.runtimetypes.*; + +public class ModuleName extends PerlModuleBase { + + public ModuleName() { + super("Module::Name", false); // false = not a pragma + } + + public static void initialize() { + ModuleName module = new ModuleName(); + try { + // Register methods - Perl name, Java method name (null = same), prototype + module.registerMethod("xs_function", null); + module.registerMethod("perl_name", "javaMethodName", null); + } catch (NoSuchMethodException e) { + System.err.println("Warning: Missing method: " + e.getMessage()); + } + } + + // Method signature: (RuntimeArray args, int ctx) -> RuntimeList + public static RuntimeList xs_function(RuntimeArray args, int ctx) { + // args.get(0) = first argument ($self for methods) + // ctx = RuntimeContextType.SCALAR, LIST, or VOID + + String param = args.get(0).toString(); + int number = args.get(1).getInt(); + + // Return value + return new RuntimeScalar(result).getList(); + } +} +``` + +### Phase 3: Create Perl Wrapper + +**File location:** `src/main/perl/lib/Module/Name.pm` + +**Template:** +```perl +package Module::Name; + +use strict; +use warnings; + +our $VERSION = '1.00'; + +# Load Java implementation +use XSLoader; +XSLoader::load('Module::Name', $VERSION); + +# Pure Perl code from original module goes here +# (accessors, helpers, overloads, etc.) + +1; + +__END__ + +=head1 NAME + +Module::Name - Description + +=head1 DESCRIPTION + +This is a port of the CPAN Module::Name module for PerlOnJava. + +=head1 AUTHOR + +Original Author Name, original@email.com + +Additional Author, other@email.com (if applicable) + +=head1 COPYRIGHT AND LICENSE + +Copyright YEAR, Original Copyright Holder. + +This module is free software; you may distribute it under the same terms +as Perl itself. + +=cut +``` + +### Phase 4: Testing + +1. **Create test file:** `src/test/resources/module_name.t` + +2. **Compare with system Perl:** + ```bash + # Create test script + cat > /tmp/test.pl << 'EOF' + use Module::Name; + # test code + EOF + + # Run with both + perl /tmp/test.pl + ./jperl /tmp/test.pl + ``` + +3. **Build and verify:** + ```bash + ./gradlew build -x test + ./jperl -e 'use Module::Name; ...' + ``` + +## Common Patterns + +### Reading XS Files + +XS files have a specific structure: + +```c +MODULE = Time::Piece PACKAGE = Time::Piece + +void +_strftime(fmt, epoch, islocal = 1) + char * fmt + time_t epoch + int islocal +CODE: + /* C implementation here */ + ST(0) = sv_2mortal(newSVpv(result, len)); +``` + +Key elements to identify: +- **Function name**: `_strftime` (usually prefixed with `_` for internal XS) +- **Parameters**: `fmt`, `epoch`, `islocal` with their C types +- **Default values**: `islocal = 1` +- **Return mechanism**: `ST(0)`, `RETVAL`, or stack manipulation + +### Converting XS to Java + +| XS Pattern | Java Equivalent | +|------------|-----------------| +| `SvIV(arg)` | `args.get(i).getInt()` | +| `SvNV(arg)` | `args.get(i).getDouble()` | +| `SvPV(arg, len)` | `args.get(i).toString()` | +| `newSViv(n)` | `new RuntimeScalar(n)` | +| `newSVnv(n)` | `new RuntimeScalar(n)` | +| `newSVpv(s, len)` | `new RuntimeScalar(s)` | +| `av_fetch(av, i, 0)` | `array.get(i)` | +| `hv_fetch(hv, k, len, 0)` | `hash.get(k)` | +| `RETVAL` / `ST(0)` | `return new RuntimeScalar(x).getList()` | + +### Using Existing Java Libraries + +**Check `build.gradle` for available dependencies:** +```bash +grep "implementation" build.gradle +``` + +**Common libraries already in PerlOnJava:** + +| Java Library | Use Case | Example Module | +|--------------|----------|----------------| +| Gson | JSON parsing/encoding | `Json.java` | +| jnr-posix | Native POSIX calls | `POSIX.java` | +| jnr-ffi | Foreign function interface | Native bindings | +| SnakeYAML | YAML parsing | `YAMLPP.java` | +| TOML4J | TOML parsing | `Toml.java` | +| Java stdlib | Crypto, encoding, time | Various | + +**Example: JSON.java uses Gson directly:** +```java +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +public static RuntimeList encode_json(RuntimeArray args, int ctx) { + Gson gson = new GsonBuilder().create(); + String json = gson.toJson(convertToJava(args.get(0))); + return new RuntimeScalar(json).getList(); +} +``` + +**Standard Java imports:** +```java +// Time operations +import java.time.*; +import java.time.format.DateTimeFormatter; + +// Crypto +import java.security.MessageDigest; + +// Encoding +import java.util.Base64; +import java.nio.charset.StandardCharsets; + +// Native POSIX calls (with Windows fallbacks) +import org.perlonjava.runtime.nativ.PosixLibrary; +import org.perlonjava.runtime.nativ.NativeUtils; +``` + +**Using PosixLibrary for native calls:** +```java +// Direct POSIX call (Unix only) +int uid = PosixLibrary.INSTANCE.getuid(); + +// Cross-platform with Windows fallback (preferred) +RuntimeScalar uid = NativeUtils.getuid(ctx); +``` + +### Returning Different Types + +```java +// Scalar +return new RuntimeScalar(value).getList(); + +// List +RuntimeList result = new RuntimeList(); +result.add(new RuntimeScalar(item1)); +result.add(new RuntimeScalar(item2)); +return result; + +// Array reference +RuntimeArray arr = new RuntimeArray(); +arr.push(new RuntimeScalar(item)); +return arr.createReference().getList(); + +// Hash reference +RuntimeHash hash = new RuntimeHash(); +hash.put("key", new RuntimeScalar(value)); +return hash.createReference().getList(); +``` + +### Handling Context + +```java +public static RuntimeList myMethod(RuntimeArray args, int ctx) { + if (ctx == RuntimeContextType.SCALAR) { + // Return single value + return new RuntimeScalar(count).getList(); + } else { + // Return list + RuntimeList result = new RuntimeList(); + for (String item : items) { + result.add(new RuntimeScalar(item)); + } + return result; + } +} +``` + +## Checklist + +### Pre-porting +- [ ] Fetch original `.pm` and `.xs` source +- [ ] Study XS code to understand C algorithms and edge cases +- [ ] Identify XS functions that need Java implementation +- [ ] Check dependencies exist in PerlOnJava +- [ ] Check `build.gradle`/`pom.xml` for usable Java libraries +- [ ] Check `nativ/` package for POSIX functionality +- [ ] Review existing similar modules for patterns + +### Implementation +- [ ] Create `ModuleName.java` with XS replacements +- [ ] Create `Module/Name.pm` with pure Perl code +- [ ] Add proper author/copyright attribution +- [ ] Register all methods in `initialize()` + +### Testing +- [ ] Build compiles without errors: `./gradlew build -x test` +- [ ] Basic functionality works: `./jperl -e 'use Module::Name; ...'` +- [ ] Compare output with system Perl +- [ ] Test edge cases identified in XS code + +### Documentation +- [ ] Add POD with AUTHOR and COPYRIGHT sections +- [ ] Credit original authors + +## Example: Time::Piece Port + +**Files created:** +- `src/main/java/org/perlonjava/runtime/perlmodule/TimePiece.java` +- `src/main/java/org/perlonjava/runtime/perlmodule/POSIX.java` (for strftime) +- `src/main/perl/lib/Time/Piece.pm` +- `src/main/perl/lib/Time/Seconds.pm` + +**XS functions replaced:** +| XS Function | Java Implementation | +|-------------|---------------------| +| `_strftime(fmt, epoch, islocal)` | `DateTimeFormatter` with format mapping | +| `_strptime(str, fmt, gmt, locale)` | `DateTimeFormatter.parse()` | +| `_tzset()` | No-op (Java handles TZ) | +| `_crt_localtime(epoch)` | `ZonedDateTime` conversion | +| `_crt_gmtime(epoch)` | `ZonedDateTime` at UTC | +| `_get_localization()` | `DateFormatSymbols` | +| `_mini_mktime(...)` | `LocalDateTime` normalization | + +**Pure Perl reused (~80%):** +- All accessor methods (sec, min, hour, year, etc.) +- Formatting helpers (ymd, hms, datetime) +- Julian day calculations +- Overloaded operators +- Import/export logic + +## Troubleshooting + +### "Can't load Java XS module" +- Check class name matches: `Module::Name` → `ModuleName.java` +- Verify `initialize()` method exists and is static +- Check package is `org.perlonjava.runtime.perlmodule` + +### Method not found +- Ensure method is registered in `initialize()` +- Check method signature: `public static RuntimeList name(RuntimeArray args, int ctx)` + +### Different output than system Perl +- Compare with fixed test values (not current time) +- Check locale handling +- Verify edge cases from XS comments + +## References + +- Module porting guide: `docs/guides/module-porting.md` +- Existing modules: `src/main/java/org/perlonjava/runtime/perlmodule/` +- Runtime types: `src/main/java/org/perlonjava/runtime/runtimetypes/` diff --git a/docs/about/changelog.md b/docs/about/changelog.md index a78315038..c3b11a41b 100644 --- a/docs/about/changelog.md +++ b/docs/about/changelog.md @@ -8,7 +8,7 @@ Release history of PerlOnJava. See [Roadmap](roadmap.md) for future plans. - Perl debugger with `-d` - Non-local control flow: `last`/`next`/`redo`/`goto LABEL` - Tail call with trampoline for `goto &NAME` and `goto __SUB__` -- Add modules: `TOML`. +- Add modules: `Time::Piece`, `TOML`. - Bugfix: operator override in Time::Hires now works. - Bugfix: internal temp variables are now pre-initialized. - Optimization: faster list assignment. diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/POSIX.java b/src/main/java/org/perlonjava/runtime/perlmodule/POSIX.java new file mode 100644 index 000000000..a460e7931 --- /dev/null +++ b/src/main/java/org/perlonjava/runtime/perlmodule/POSIX.java @@ -0,0 +1,309 @@ +package org.perlonjava.runtime.perlmodule; + +import org.perlonjava.runtime.nativ.NativeUtils; +import org.perlonjava.runtime.nativ.PosixLibrary; +import org.perlonjava.runtime.operators.Time; +import org.perlonjava.runtime.runtimetypes.*; + +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.time.format.TextStyle; +import java.time.temporal.WeekFields; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class POSIX extends PerlModuleBase { + + private static final Pattern FORMAT_PATTERN = Pattern.compile("%([EO])?([%aAbBcCdDeFgGhHIjmMnpPrRsStTuUVwWxXyYzZ])"); + + public POSIX() { + super("POSIX", false); + } + + public static void initialize() { + POSIX module = new POSIX(); + try { + module.registerMethod("_strftime", "strftime", null); + module.registerMethod("_mktime", "mktime", null); + module.registerMethod("_time", "posix_time", null); + module.registerMethod("_sleep", "posix_sleep", null); + module.registerMethod("_alarm", "posix_alarm", null); + module.registerMethod("_getpid", "getpid", null); + module.registerMethod("_getppid", "getppid", null); + module.registerMethod("_getuid", "getuid", null); + module.registerMethod("_geteuid", "geteuid", null); + module.registerMethod("_getgid", "getgid", null); + module.registerMethod("_getegid", "getegid", null); + module.registerMethod("_getcwd", "getcwd", null); + module.registerMethod("_strerror", "strerror", null); + } catch (NoSuchMethodException e) { + System.err.println("Warning: Missing POSIX method: " + e.getMessage()); + } + } + + /** + * POSIX strftime - convert date and time to string. + * Arguments: fmt, sec, min, hour, mday, mon, year, [wday, yday, isdst] + */ + public static RuntimeList strftime(RuntimeArray args, int ctx) { + if (args.size() < 7) { + throw new IllegalArgumentException("strftime requires at least 7 arguments"); + } + + String format = args.get(0).toString(); + int sec = args.get(1).getInt(); + int min = args.get(2).getInt(); + int hour = args.get(3).getInt(); + int mday = args.get(4).getInt(); + int mon = args.get(5).getInt(); // 0-based + int year = args.get(6).getInt(); // years since 1900 + + // wday, yday, isdst are ignored as per POSIX spec - they're computed from other values + int actualYear = year + 1900; + int actualMon = mon + 1; // Convert to 1-based for Java + + // Create LocalDateTime + LocalDateTime dateTime; + try { + dateTime = LocalDateTime.of(actualYear, actualMon, mday, hour, min, sec); + } catch (Exception e) { + return new RuntimeScalar("").getList(); + } + + // Get timezone info for %z and %Z + ZonedDateTime zonedDateTime = dateTime.atZone(ZoneId.systemDefault()); + + String result = formatStrftime(format, zonedDateTime); + return new RuntimeScalar(result).getList(); + } + + /** + * Format a ZonedDateTime using strftime format codes. + */ + public static String formatStrftime(String format, ZonedDateTime dt) { + StringBuffer result = new StringBuffer(); + Matcher m = FORMAT_PATTERN.matcher(format); + + while (m.find()) { + String modifier = m.group(1); // E or O modifier (ignored for now) + String code = m.group(2); + String replacement = formatCode(code, dt); + m.appendReplacement(result, Matcher.quoteReplacement(replacement)); + } + m.appendTail(result); + + return result.toString(); + } + + /** + * Calculate POSIX-style week number where days before the first occurrence + * of the specified day are week 0. + * %U: Sunday as first day of week + * %W: Monday as first day of week + */ + private static int calculateWeekNumber(ZonedDateTime dt, DayOfWeek firstDayOfWeek) { + int dayOfYear = dt.getDayOfYear(); // 1-based + + // Find the day of week of Jan 1 + LocalDate jan1 = LocalDate.of(dt.getYear(), 1, 1); + DayOfWeek jan1Dow = jan1.getDayOfWeek(); + + // Calculate day of year (1-based) of the first occurrence of firstDayOfWeek + // Java DayOfWeek: MONDAY=1, TUESDAY=2, ... SUNDAY=7 + int jan1DowValue = jan1Dow.getValue(); // 1-7 + int firstDowValue = firstDayOfWeek.getValue(); // 1-7 + + int daysUntilFirstWeekStart = (firstDowValue - jan1DowValue + 7) % 7; + // If Jan 1 is the firstDayOfWeek, daysUntilFirstWeekStart is 0 + int firstWeekStartDoy = 1 + daysUntilFirstWeekStart; // day of year when week 1 starts + + if (dayOfYear < firstWeekStartDoy) { + return 0; // Before the first occurrence of firstDayOfWeek + } + + // Week 1 starts on firstWeekStartDoy + return (dayOfYear - firstWeekStartDoy) / 7 + 1; + } + + private static String formatCode(String code, ZonedDateTime dt) { + Locale locale = Locale.getDefault(); + + switch (code) { + case "%": return "%"; + case "a": return dt.getDayOfWeek().getDisplayName(TextStyle.SHORT, locale); + case "A": return dt.getDayOfWeek().getDisplayName(TextStyle.FULL, locale); + case "b": + case "h": return dt.getMonth().getDisplayName(TextStyle.SHORT, locale); + case "B": return dt.getMonth().getDisplayName(TextStyle.FULL, locale); + case "c": return dt.format(DateTimeFormatter.ofPattern("EEE MMM d HH:mm:ss yyyy", locale)); + case "C": return String.format("%02d", dt.getYear() / 100); + case "d": return String.format("%02d", dt.getDayOfMonth()); + case "D": return dt.format(DateTimeFormatter.ofPattern("MM/dd/yy", locale)); + case "e": return String.format("%2d", dt.getDayOfMonth()); + case "F": return dt.format(DateTimeFormatter.ISO_LOCAL_DATE); + case "g": { + // ISO week-based year, last 2 digits + int weekYear = dt.get(WeekFields.ISO.weekBasedYear()); + return String.format("%02d", weekYear % 100); + } + case "G": { + // ISO week-based year, 4 digits + return String.format("%04d", dt.get(WeekFields.ISO.weekBasedYear())); + } + case "H": return String.format("%02d", dt.getHour()); + case "I": { + int hour12 = dt.getHour() % 12; + return String.format("%02d", hour12 == 0 ? 12 : hour12); + } + case "j": return String.format("%03d", dt.getDayOfYear()); + case "m": return String.format("%02d", dt.getMonthValue()); + case "M": return String.format("%02d", dt.getMinute()); + case "n": return "\n"; + case "p": return dt.getHour() < 12 ? "AM" : "PM"; + case "P": return dt.getHour() < 12 ? "am" : "pm"; + case "r": { + int hour12 = dt.getHour() % 12; + hour12 = hour12 == 0 ? 12 : hour12; + return String.format("%02d:%02d:%02d %s", hour12, dt.getMinute(), dt.getSecond(), + dt.getHour() < 12 ? "AM" : "PM"); + } + case "R": return String.format("%02d:%02d", dt.getHour(), dt.getMinute()); + case "s": return String.valueOf(dt.toEpochSecond()); + case "S": return String.format("%02d", dt.getSecond()); + case "t": return "\t"; + case "T": return String.format("%02d:%02d:%02d", dt.getHour(), dt.getMinute(), dt.getSecond()); + case "u": { + // Monday=1 .. Sunday=7 + int dow = dt.getDayOfWeek().getValue(); + return String.valueOf(dow); + } + case "U": { + // Week number (Sunday as first day), 00-53 + // Days before first Sunday of year are week 0 + int weekNum = calculateWeekNumber(dt, DayOfWeek.SUNDAY); + return String.format("%02d", weekNum); + } + case "V": { + // ISO week number + int weekNum = dt.get(WeekFields.ISO.weekOfWeekBasedYear()); + return String.format("%02d", weekNum); + } + case "w": { + // Sunday=0 .. Saturday=6 + int dow = dt.getDayOfWeek().getValue() % 7; + return String.valueOf(dow); + } + case "W": { + // Week number (Monday as first day), 00-53 + // Days before first Monday of year are week 0 + int weekNum = calculateWeekNumber(dt, DayOfWeek.MONDAY); + return String.format("%02d", weekNum); + } + case "x": return dt.format(DateTimeFormatter.ofPattern("MM/dd/yy", locale)); + case "X": return dt.format(DateTimeFormatter.ofPattern("HH:mm:ss", locale)); + case "y": return String.format("%02d", dt.getYear() % 100); + case "Y": return String.format("%04d", dt.getYear()); + case "z": { + ZoneOffset offset = dt.getOffset(); + int totalSeconds = offset.getTotalSeconds(); + int hours = totalSeconds / 3600; + int minutes = Math.abs((totalSeconds % 3600) / 60); + return String.format("%+03d%02d", hours, minutes); + } + case "Z": { + return dt.getZone().getDisplayName(TextStyle.SHORT, locale); + } + default: return "%" + code; + } + } + + /** + * POSIX mktime - convert time structure to epoch. + * Arguments: sec, min, hour, mday, mon, year, [wday, yday, isdst] + */ + public static RuntimeList mktime(RuntimeArray args, int ctx) { + if (args.size() < 6) { + return new RuntimeScalar(-1).getList(); + } + + int sec = args.get(0).getInt(); + int min = args.get(1).getInt(); + int hour = args.get(2).getInt(); + int mday = args.get(3).getInt(); + int mon = args.get(4).getInt(); + int year = args.get(5).getInt(); + + int actualYear = year + 1900; + int actualMon = mon + 1; + + try { + LocalDateTime ldt = LocalDateTime.of(actualYear, actualMon, mday, hour, min, sec); + ZonedDateTime zdt = ldt.atZone(ZoneId.systemDefault()); + return new RuntimeScalar(zdt.toEpochSecond()).getList(); + } catch (Exception e) { + return new RuntimeScalar(-1).getList(); + } + } + + public static RuntimeList posix_time(RuntimeArray args, int ctx) { + return Time.time().getList(); + } + + public static RuntimeList posix_sleep(RuntimeArray args, int ctx) { + if (args.isEmpty()) { + return new RuntimeScalar(0).getList(); + } + return Time.sleep(args.get(0)).getList(); + } + + public static RuntimeList posix_alarm(RuntimeArray args, int ctx) { + if (args.isEmpty()) { + return Time.alarm(ctx).getList(); + } + return Time.alarm(ctx, args.get(0)).getList(); + } + + public static RuntimeList getpid(RuntimeArray args, int ctx) { + return new RuntimeScalar(ProcessHandle.current().pid()).getList(); + } + + public static RuntimeList getppid(RuntimeArray args, int ctx) { + return NativeUtils.getppid(ctx).getList(); + } + + public static RuntimeList getuid(RuntimeArray args, int ctx) { + return NativeUtils.getuid(ctx).getList(); + } + + public static RuntimeList geteuid(RuntimeArray args, int ctx) { + return NativeUtils.geteuid(ctx).getList(); + } + + public static RuntimeList getgid(RuntimeArray args, int ctx) { + return NativeUtils.getgid(ctx).getList(); + } + + public static RuntimeList getegid(RuntimeArray args, int ctx) { + return NativeUtils.getegid(ctx).getList(); + } + + public static RuntimeList getcwd(RuntimeArray args, int ctx) { + return new RuntimeScalar(System.getProperty("user.dir")).getList(); + } + + public static RuntimeList strerror(RuntimeArray args, int ctx) { + if (args.isEmpty()) { + return new RuntimeScalar("").getList(); + } + int errno = args.get(0).getInt(); + // Return a basic error message - could be enhanced with actual errno mapping + String msg = "Error " + errno; + try { + msg = org.perlonjava.runtime.nativ.PosixLibrary.INSTANCE.strerror(errno); + } catch (Exception e) { + // Fall back to generic message + } + return new RuntimeScalar(msg).getList(); + } +} diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/TimePiece.java b/src/main/java/org/perlonjava/runtime/perlmodule/TimePiece.java new file mode 100644 index 000000000..b66a0a5cc --- /dev/null +++ b/src/main/java/org/perlonjava/runtime/perlmodule/TimePiece.java @@ -0,0 +1,348 @@ +package org.perlonjava.runtime.perlmodule; + +import org.perlonjava.runtime.operators.Time; +import org.perlonjava.runtime.runtimetypes.*; + +import java.text.DateFormatSymbols; +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.temporal.ChronoField; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class TimePiece extends PerlModuleBase { + + public TimePiece() { + super("Time::Piece", false); + } + + public static void initialize() { + TimePiece module = new TimePiece(); + try { + module.registerMethod("_strftime", null); + module.registerMethod("_strptime", null); + module.registerMethod("_tzset", null); + module.registerMethod("_crt_localtime", null); + module.registerMethod("_crt_gmtime", null); + module.registerMethod("_get_localization", null); + module.registerMethod("_mini_mktime", null); + } catch (NoSuchMethodException e) { + System.err.println("Warning: Missing Time::Piece method: " + e.getMessage()); + } + } + + /** + * _strftime(format, epoch, islocal) + * Format a time value using strftime-style format codes. + */ + public static RuntimeList _strftime(RuntimeArray args, int ctx) { + if (args.size() < 3) { + return new RuntimeScalar("").getList(); + } + + String format = args.get(0).toString(); + long epoch = args.get(1).getLong(); + boolean isLocal = args.get(2).getBoolean(); + + ZonedDateTime dt; + if (isLocal) { + dt = Instant.ofEpochSecond(epoch).atZone(ZoneId.systemDefault()); + } else { + dt = Instant.ofEpochSecond(epoch).atZone(ZoneOffset.UTC); + } + + String result = POSIX.formatStrftime(format, dt); + return new RuntimeScalar(result).getList(); + } + + /** + * _strptime(string, format, islocal, locales) + * Parse a time string using strftime-style format codes. + * Returns array: (sec, min, hour, mday, mon, year, wday, yday, isdst, epoch, islocal) + */ + public static RuntimeList _strptime(RuntimeArray args, int ctx) { + if (args.size() < 3) { + return new RuntimeList(); + } + + String dateString = args.get(0).toString(); + String format = args.get(1).toString(); + boolean isLocal = args.get(2).getBoolean(); + RuntimeHash locales = args.size() > 3 ? args.get(3).hashDeref() : null; + + // Convert strftime format to Java DateTimeFormatter pattern + String javaPattern = convertStrftimeToJava(format, locales); + + try { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(javaPattern, Locale.getDefault()); + + // Try to parse - we need to handle partial dates + LocalDateTime parsedDateTime = parseFlexible(dateString, formatter, javaPattern); + + if (parsedDateTime == null) { + return new RuntimeList(); + } + + ZonedDateTime zdt; + if (isLocal) { + zdt = parsedDateTime.atZone(ZoneId.systemDefault()); + } else { + zdt = parsedDateTime.atZone(ZoneOffset.UTC); + } + + return buildTimeArray(zdt, isLocal); + } catch (Exception e) { + return new RuntimeList(); + } + } + + private static LocalDateTime parseFlexible(String dateString, DateTimeFormatter formatter, String pattern) { + try { + // Try full LocalDateTime parse first + return LocalDateTime.parse(dateString, formatter); + } catch (DateTimeParseException e) { + // Try LocalDate only + try { + LocalDate date = LocalDate.parse(dateString, formatter); + return date.atStartOfDay(); + } catch (DateTimeParseException e2) { + // Try LocalTime only + try { + LocalTime time = LocalTime.parse(dateString, formatter); + return LocalDateTime.of(LocalDate.now(), time); + } catch (DateTimeParseException e3) { + return null; + } + } + } + } + + private static String convertStrftimeToJava(String format, RuntimeHash locales) { + StringBuilder result = new StringBuilder(); + int i = 0; + while (i < format.length()) { + char c = format.charAt(i); + if (c == '%' && i + 1 < format.length()) { + char code = format.charAt(i + 1); + String replacement = strftimeCodeToJava(code); + result.append(replacement); + i += 2; + } else if (c == '\'') { + result.append("''"); + i++; + } else if (Character.isLetter(c)) { + result.append("'").append(c).append("'"); + i++; + } else { + result.append(c); + i++; + } + } + return result.toString(); + } + + private static String strftimeCodeToJava(char code) { + switch (code) { + case '%': return "%"; + case 'a': return "EEE"; + case 'A': return "EEEE"; + case 'b': case 'h': return "MMM"; + case 'B': return "MMMM"; + case 'c': return "EEE MMM d HH:mm:ss yyyy"; + case 'C': return "yy"; // Approximation - century + case 'd': return "dd"; + case 'D': return "MM/dd/yy"; + case 'e': return "d"; + case 'F': return "yyyy-MM-dd"; + case 'H': return "HH"; + case 'I': return "hh"; + case 'j': return "DDD"; + case 'm': return "MM"; + case 'M': return "mm"; + case 'n': return "\n"; + case 'p': return "a"; + case 'P': return "a"; // lowercase am/pm + case 'r': return "hh:mm:ss a"; + case 'R': return "HH:mm"; + case 'S': return "ss"; + case 't': return "\t"; + case 'T': return "HH:mm:ss"; + case 'u': return "u"; // day of week 1-7 + case 'U': return "ww"; // week of year + case 'V': return "ww"; // ISO week + case 'w': return "u"; // day of week (will need adjustment) + case 'W': return "ww"; + case 'x': return "MM/dd/yy"; + case 'X': return "HH:mm:ss"; + case 'y': return "yy"; + case 'Y': return "yyyy"; + case 'z': return "Z"; + case 'Z': return "z"; + default: return "%" + code; + } + } + + /** + * _tzset() - Initialize timezone. No-op in Java as TZ is handled automatically. + */ + public static RuntimeList _tzset(RuntimeArray args, int ctx) { + // Java handles timezone automatically + return new RuntimeList(); + } + + /** + * _crt_localtime(epoch) - Return localtime array for given epoch. + */ + public static RuntimeList _crt_localtime(RuntimeArray args, int ctx) { + long epoch = args.isEmpty() ? System.currentTimeMillis() / 1000 : args.get(0).getLong(); + ZonedDateTime dt = Instant.ofEpochSecond(epoch).atZone(ZoneId.systemDefault()); + return buildTimeList(dt); + } + + /** + * _crt_gmtime(epoch) - Return gmtime array for given epoch. + */ + public static RuntimeList _crt_gmtime(RuntimeArray args, int ctx) { + long epoch = args.isEmpty() ? System.currentTimeMillis() / 1000 : args.get(0).getLong(); + ZonedDateTime dt = Instant.ofEpochSecond(epoch).atZone(ZoneOffset.UTC); + return buildTimeList(dt); + } + + private static RuntimeList buildTimeList(ZonedDateTime dt) { + RuntimeList result = new RuntimeList(); + result.add(new RuntimeScalar(dt.getSecond())); + result.add(new RuntimeScalar(dt.getMinute())); + result.add(new RuntimeScalar(dt.getHour())); + result.add(new RuntimeScalar(dt.getDayOfMonth())); + result.add(new RuntimeScalar(dt.getMonthValue() - 1)); // 0-based + result.add(new RuntimeScalar(dt.getYear() - 1900)); + // wday: Java 1=Mon..7=Sun, Perl 0=Sun..6=Sat + result.add(new RuntimeScalar(dt.getDayOfWeek().getValue() % 7)); + result.add(new RuntimeScalar(dt.getDayOfYear() - 1)); // 0-based + result.add(new RuntimeScalar(dt.getZone().getRules().isDaylightSavings(dt.toInstant()) ? 1 : 0)); + return result; + } + + private static RuntimeList buildTimeArray(ZonedDateTime dt, boolean isLocal) { + RuntimeList result = new RuntimeList(); + result.add(new RuntimeScalar(dt.getSecond())); + result.add(new RuntimeScalar(dt.getMinute())); + result.add(new RuntimeScalar(dt.getHour())); + result.add(new RuntimeScalar(dt.getDayOfMonth())); + result.add(new RuntimeScalar(dt.getMonthValue() - 1)); // 0-based + result.add(new RuntimeScalar(dt.getYear() - 1900)); + result.add(new RuntimeScalar(dt.getDayOfWeek().getValue() % 7)); + result.add(new RuntimeScalar(dt.getDayOfYear() - 1)); + result.add(new RuntimeScalar(dt.getZone().getRules().isDaylightSavings(dt.toInstant()) ? 1 : 0)); + result.add(new RuntimeScalar(dt.toEpochSecond())); + result.add(new RuntimeScalar(isLocal ? 1 : 0)); + return result; + } + + /** + * _get_localization() - Return hash with locale-specific day/month names. + */ + public static RuntimeList _get_localization(RuntimeArray args, int ctx) { + RuntimeHash result = new RuntimeHash(); + DateFormatSymbols symbols = DateFormatSymbols.getInstance(Locale.getDefault()); + + // Weekday names (Sunday first for Perl compatibility) + RuntimeArray weekday = new RuntimeArray(); + RuntimeArray wday = new RuntimeArray(); + String[] weekdays = symbols.getWeekdays(); + String[] shortWeekdays = symbols.getShortWeekdays(); + // Java: index 1=Sunday, 2=Monday, ... 7=Saturday + for (int i = 1; i <= 7; i++) { + weekday.push(new RuntimeScalar(weekdays[i])); + wday.push(new RuntimeScalar(shortWeekdays[i])); + } + result.put("weekday", weekday.createReference()); + result.put("wday", wday.createReference()); + + // Month names + RuntimeArray month = new RuntimeArray(); + RuntimeArray mon = new RuntimeArray(); + String[] months = symbols.getMonths(); + String[] shortMonths = symbols.getShortMonths(); + for (int i = 0; i < 12; i++) { + month.push(new RuntimeScalar(months[i])); + mon.push(new RuntimeScalar(shortMonths[i])); + } + result.put("month", month.createReference()); + result.put("mon", mon.createReference()); + result.put("alt_month", month.createReference()); + + // AM/PM + String[] ampm = symbols.getAmPmStrings(); + result.put("AM", new RuntimeScalar(ampm[0])); + result.put("PM", new RuntimeScalar(ampm[1])); + result.put("am", new RuntimeScalar(ampm[0].toLowerCase())); + result.put("pm", new RuntimeScalar(ampm[1].toLowerCase())); + + result.put("c_fmt", new RuntimeScalar("")); + + return result.createReference().getList(); + } + + /** + * _mini_mktime(sec, min, hour, mday, mon, year) + * Normalize time values and return array. + * Used by add_months to handle month overflow. + */ + public static RuntimeList _mini_mktime(RuntimeArray args, int ctx) { + if (args.size() < 6) { + return new RuntimeList(); + } + + int sec = args.get(0).getInt(); + int min = args.get(1).getInt(); + int hour = args.get(2).getInt(); + int mday = args.get(3).getInt(); + int mon = args.get(4).getInt(); + int year = args.get(5).getInt(); // years since 1900 + + // Normalize the values by creating a LocalDateTime and letting Java handle overflow + try { + // Handle month overflow + int extraYears = mon / 12; + mon = mon % 12; + if (mon < 0) { + mon += 12; + extraYears--; + } + year += extraYears; + + int actualYear = year + 1900; + int actualMon = mon + 1; + + // Clamp day to valid range for the month + YearMonth ym = YearMonth.of(actualYear, actualMon); + int maxDay = ym.lengthOfMonth(); + if (mday > maxDay) { + mday = maxDay; + } + if (mday < 1) { + mday = 1; + } + + LocalDateTime dt = LocalDateTime.of(actualYear, actualMon, mday, hour, min, sec); + ZonedDateTime zdt = dt.atZone(ZoneId.systemDefault()); + + RuntimeList result = new RuntimeList(); + result.add(new RuntimeScalar(dt.getSecond())); + result.add(new RuntimeScalar(dt.getMinute())); + result.add(new RuntimeScalar(dt.getHour())); + result.add(new RuntimeScalar(dt.getDayOfMonth())); + result.add(new RuntimeScalar(dt.getMonthValue() - 1)); + result.add(new RuntimeScalar(dt.getYear() - 1900)); + result.add(new RuntimeScalar(dt.getDayOfWeek().getValue() % 7)); + result.add(new RuntimeScalar(dt.getDayOfYear() - 1)); + result.add(new RuntimeScalar(zdt.getZone().getRules().isDaylightSavings(zdt.toInstant()) ? 1 : 0)); + return result; + } catch (Exception e) { + return new RuntimeList(); + } + } +} diff --git a/src/main/perl/lib/POSIX.pm b/src/main/perl/lib/POSIX.pm index 1a13dde1c..bb0a6f8ce 100644 --- a/src/main/perl/lib/POSIX.pm +++ b/src/main/perl/lib/POSIX.pm @@ -289,6 +289,24 @@ BEGIN { } } +# Time functions +sub strftime { + my ($fmt, $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = @_; + # wday, yday, isdst are ignored per POSIX spec + $wday //= -1; + $yday //= -1; + $isdst //= -1; + return POSIX::_strftime($fmt, $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst); +} + +sub mktime { + my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = @_; + $wday //= -1; + $yday //= -1; + $isdst //= -1; + return POSIX::_mktime($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst); +} + # Exit status macros sub WIFEXITED { POSIX::_WIFEXITED(@_) } sub WEXITSTATUS { POSIX::_WEXITSTATUS(@_) } diff --git a/src/main/perl/lib/Time/Piece.pm b/src/main/perl/lib/Time/Piece.pm new file mode 100644 index 000000000..c8fd256cb --- /dev/null +++ b/src/main/perl/lib/Time/Piece.pm @@ -0,0 +1,763 @@ +package Time::Piece; + +use strict; +use warnings; + +use XSLoader; +use Time::Seconds; +use Carp; +use Time::Local; +use Scalar::Util qw/ blessed /; +use Exporter (); + +our @EXPORT = qw( localtime gmtime ); +our %EXPORT_TAGS = ( ':override' => 'internal' ); +our $VERSION = '1.3401'; + +XSLoader::load('Time::Piece', $VERSION); + +my $DATE_SEP = '-'; +my $TIME_SEP = ':'; +my @MON_LIST = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec); +my @FULLMON_LIST = qw(January February March April May June July + August September October November December); +my @DAY_LIST = qw(Sun Mon Tue Wed Thu Fri Sat); +my @FULLDAY_LIST = qw(Sunday Monday Tuesday Wednesday Thursday Friday Saturday); + +my $IS_WIN32 = ($^O =~ /Win32/); +my $LOCALE; + +use constant { + 'c_sec' => 0, + 'c_min' => 1, + 'c_hour' => 2, + 'c_mday' => 3, + 'c_mon' => 4, + 'c_year' => 5, + 'c_wday' => 6, + 'c_yday' => 7, + 'c_isdst' => 8, + 'c_epoch' => 9, + 'c_islocal' => 10, +}; + +sub localtime { + unshift @_, __PACKAGE__ unless eval { $_[0]->isa('Time::Piece') }; + my $class = shift; + my $time = shift; + $time = time if (!defined $time); + $class->_mktime($time, 1); +} + +sub gmtime { + unshift @_, __PACKAGE__ unless eval { $_[0]->isa('Time::Piece') }; + my $class = shift; + my $time = shift; + $time = time if (!defined $time); + $class->_mktime($time, 0); +} + +sub _is_time_struct { + return 1 if ref($_[1]) eq 'ARRAY'; + return 1 if blessed($_[1]) && $_[1]->isa('Time::Piece'); + return 0; +} + +sub new { + my $class = shift; + my ($time) = @_; + + my $self; + + if ($class->_is_time_struct($time)) { + $self = $time->[c_islocal] ? + $class->localtime($time) : $class->gmtime($time); + } + elsif (defined($time)) { + $self = $class->localtime($time); + } + elsif (ref($class) && $class->isa(__PACKAGE__)) { + $self = $class->_mktime($class->epoch, $class->[c_islocal]); + } + else { + $self = $class->localtime(); + } + + return bless $self, ref($class) || $class; +} + +sub _mktime { + my ($class, $time, $islocal) = @_; + $class = blessed($class) || $class; + + if ($class->_is_time_struct($time)) { + my @new_time = @$time; + my @tm_parts = (@new_time[c_sec .. c_mon], $new_time[c_year]+1900); + $new_time[c_epoch] = $islocal ? timelocal(@tm_parts) : timegm(@tm_parts); + return wantarray ? @new_time : bless [@new_time[0..9], $islocal], $class; + } + + _tzset(); + my @time = $islocal ? + CORE::localtime($time) : CORE::gmtime($time); + wantarray ? @time : bless [@time, $time, $islocal], $class; +} + +my %_special_exports = ( + localtime => sub { my $c = $_[0]; sub { $c->localtime(@_) } }, + gmtime => sub { my $c = $_[0]; sub { $c->gmtime(@_) } }, +); + +sub export { + my ($class, $to, @methods) = @_; + for my $method (@methods) { + if (exists $_special_exports{$method}) { + no strict 'refs'; + no warnings 'redefine'; + *{$to . "::$method"} = $_special_exports{$method}->($class); + } else { + $class->Exporter::export($to, $method); + } + } +} + +sub import { + my $class = shift; + my %params; + map($params{$_}++,@_,@EXPORT); + if (delete $params{':override'}) { + $class->export('CORE::GLOBAL', keys %params); + } else { + $class->export(scalar caller, keys %params); + } +} + +## Methods ## + +sub sec { my $time = shift; $time->[c_sec]; } +*second = \&sec; + +sub min { my $time = shift; $time->[c_min]; } +*minute = \&min; + +sub hour { my $time = shift; $time->[c_hour]; } + +sub mday { my $time = shift; $time->[c_mday]; } +*day_of_month = \&mday; + +sub mon { my $time = shift; $time->[c_mon] + 1; } + +sub _mon { my $time = shift; $time->[c_mon]; } + +sub month { + my $time = shift; + if (@_) { + return $_[$time->[c_mon]]; + } + elsif (@MON_LIST) { + return $MON_LIST[$time->[c_mon]]; + } + else { + return $time->strftime('%b'); + } +} +*monname = \&month; + +sub fullmonth { + my $time = shift; + if (@_) { + return $_[$time->[c_mon]]; + } + elsif (@FULLMON_LIST) { + return $FULLMON_LIST[$time->[c_mon]]; + } + else { + return $time->strftime('%B'); + } +} + +sub year { my $time = shift; $time->[c_year] + 1900; } + +sub _year { my $time = shift; $time->[c_year]; } + +sub yy { + my $time = shift; + my $res = $time->[c_year] % 100; + return $res > 9 ? $res : "0$res"; +} + +sub wday { my $time = shift; $time->[c_wday] + 1; } + +sub _wday { my $time = shift; $time->[c_wday]; } +*day_of_week = \&_wday; + +sub wdayname { + my $time = shift; + if (@_) { + return $_[$time->[c_wday]]; + } + elsif (@DAY_LIST) { + return $DAY_LIST[$time->[c_wday]]; + } + else { + return $time->strftime('%a'); + } +} +*day = \&wdayname; + +sub fullday { + my $time = shift; + if (@_) { + return $_[$time->[c_wday]]; + } + elsif (@FULLDAY_LIST) { + return $FULLDAY_LIST[$time->[c_wday]]; + } + else { + return $time->strftime('%A'); + } +} + +sub yday { my $time = shift; $time->[c_yday]; } +*day_of_year = \&yday; + +sub isdst { my $time = shift; $time->[c_isdst]; } +*daylight_savings = \&isdst; + +sub tzoffset { + my $time = shift; + return Time::Seconds->new(0) unless $time->[c_islocal]; + + my $epoch = $time->epoch; + + my $j = sub { + my ($s,$n,$h,$d,$m,$y) = @_; + $m += 1; + $y += 1900; + $time->_jd($y, $m, $d, $h, $n, $s); + }; + + my $delta = 24 * ($j->(_crt_localtime($epoch)) - $j->(_crt_gmtime($epoch))); + + return Time::Seconds->new( int($delta * 60 + ($delta >= 0 ? 0.5 : -0.5)) * 60 ); +} + +sub epoch { + my $time = shift; + if (defined($time->[c_epoch])) { + return $time->[c_epoch]; + } + else { + my $epoch = $time->[c_islocal] ? + timelocal(@{$time}[c_sec .. c_mon], $time->[c_year]+1900) : + timegm(@{$time}[c_sec .. c_mon], $time->[c_year]+1900); + $time->[c_epoch] = $epoch; + return $epoch; + } +} + +sub hms { + my $time = shift; + my $sep = @_ ? shift(@_) : $TIME_SEP; + sprintf("%02d$sep%02d$sep%02d", $time->[c_hour], $time->[c_min], $time->[c_sec]); +} +*time = \&hms; + +sub ymd { + my $time = shift; + my $sep = @_ ? shift(@_) : $DATE_SEP; + sprintf("%d$sep%02d$sep%02d", $time->year, $time->mon, $time->[c_mday]); +} +*date = \&ymd; + +sub mdy { + my $time = shift; + my $sep = @_ ? shift(@_) : $DATE_SEP; + sprintf("%02d$sep%02d$sep%d", $time->mon, $time->[c_mday], $time->year); +} + +sub dmy { + my $time = shift; + my $sep = @_ ? shift(@_) : $DATE_SEP; + sprintf("%02d$sep%02d$sep%d", $time->[c_mday], $time->mon, $time->year); +} + +sub datetime { + my $time = shift; + my %seps = (date => $DATE_SEP, T => 'T', time => $TIME_SEP, @_); + return join($seps{T}, $time->date($seps{date}), $time->time($seps{time})); +} + +sub julian_day { + my $time = shift; + $time = $time->gmtime( $time->epoch ) if $time->[c_islocal]; + my $jd = $time->_jd( $time->year, $time->mon, $time->mday, + $time->hour, $time->min, $time->sec); + return $jd; +} + +sub mjd { + return shift->julian_day - 2_400_000.5; +} + +sub _jd { + my $self = shift; + my ($y, $m, $d, $h, $n, $s) = @_; + + $y = ( $m > 2 ? $y : $y - 1); + $m = ( $m > 2 ? $m - 3 : $m + 9); + + my $J = int( 365.25 *( $y + 4712) ) + + int( (30.6 * $m) + 0.5) + 59 + $d - 0.5; + + my $G = 38 - int( 0.75 * int(49+($y/100))); + + my $JD = $J + $G; + + return $JD + ($h + ($n + $s / 60) / 60) / 24; +} + +sub week { + my $self = shift; + my $J = $self->julian_day; + + $J += ($self->tzoffset/(24*3600)) if $self->[c_islocal]; + + $J = int($J+0.5); + + use integer; + my $d4 = ((($J + 31741 - ($J % 7)) % 146097) % 36524) % 1461; + my $L = $d4 / 1460; + my $d1 = (($d4 - $L) % 365) + $L; + return $d1 / 7 + 1; +} + +sub _is_leap_year { + my $year = shift; + return (($year %4 == 0) && !($year % 100 == 0)) || ($year % 400 == 0) + ? 1 : 0; +} + +sub is_leap_year { + my $time = shift; + my $year = $time->year; + return _is_leap_year($year); +} + +my @MON_LAST = qw(31 28 31 30 31 30 31 31 30 31 30 31); + +sub month_last_day { + my $time = shift; + my $year = $time->year; + my $_mon = $time->_mon; + return $MON_LAST[$_mon] + ($_mon == 1 ? _is_leap_year($year) : 0); +} + +my $trans_map_common = { + 'c' => sub { + my ( $format ) = @_; + if($LOCALE->{PM} && $LOCALE->{AM}){ + $format =~ s/%c/%a %d %b %Y %I:%M:%S %p/; + } else { + $format =~ s/%c/%a %d %b %Y %H:%M:%S/; + } + return $format; + }, + 'r' => sub { + my ( $format ) = @_; + if($LOCALE->{PM} && $LOCALE->{AM}){ + $format =~ s/%r/%I:%M:%S %p/; + } else { + $format =~ s/%r/%H:%M:%S/; + } + return $format; + }, + 'X' => sub { + my ( $format ) = @_; + if($LOCALE->{PM} && $LOCALE->{AM}){ + $format =~ s/%X/%I:%M:%S %p/; + } else { + $format =~ s/%X/%H:%M:%S/; + } + return $format; + }, +}; + +my $strftime_trans_map = { + %{$trans_map_common}, + 'e' => sub { + my ( $format, $time ) = @_; + $format =~ s/%e/%d/ if $IS_WIN32; + return $format; + }, + 'D' => sub { + my ( $format, $time ) = @_; + $format =~ s/%D/%m\/%d\/%y/; + return $format; + }, + 'F' => sub { + my ( $format, $time ) = @_; + $format =~ s/%F/%Y-%m-%d/; + return $format; + }, + 'R' => sub { + my ( $format, $time ) = @_; + $format =~ s/%R/%H:%M/; + return $format; + }, + 's' => sub { + my ( $format, $time ) = @_; + $format =~ s/%s/$time->[c_epoch]/; + return $format; + }, + 'T' => sub { + my ( $format, $time ) = @_; + $format =~ s/%T/%H:%M:%S/ if $IS_WIN32; + return $format; + }, + 'u' => sub { + my ( $format, $time ) = @_; + $format =~ s/%u/%w/ if $IS_WIN32; + return $format; + }, + 'V' => sub { + my ( $format, $time ) = @_; + my $week = sprintf( "%02d", $time->week() ); + $format =~ s/%V/$week/ if $IS_WIN32; + return $format; + }, + 'x' => sub { + my ( $format, $time ) = @_; + $format =~ s/%x/%a %d %b %Y/; + return $format; + }, + 'z' => sub { + my ( $format, $time ) = @_; + $format =~ s/%z/+0000/ if not $time->[c_islocal]; + return $format; + }, + 'Z' => sub { + my ( $format, $time ) = @_; + $format =~ s/%Z/UTC/ if not $time->[c_islocal]; + return $format; + }, +}; + +sub strftime { + my $time = shift; + my $format = @_ ? shift(@_) : '%a, %d %b %Y %H:%M:%S %Z'; + $format = _translate_format($format, $strftime_trans_map, $time); + return $format unless $format =~ /%/; + return _strftime($format, $time->epoch, $time->[c_islocal]); +} + +my $strptime_trans_map = { + %{$trans_map_common}, +}; + +sub strptime { + my $time = shift; + my $string = shift; + my $format = @_ ? shift(@_) : "%a, %d %b %Y %H:%M:%S %Z"; + my $islocal = (ref($time) ? $time->[c_islocal] : 0); + my $locales = $LOCALE || &Time::Piece::_default_locale(); + $format = _translate_format($format, $strptime_trans_map); + my @vals = _strptime($string, $format, $islocal, $locales); + return scalar $time->_mktime(\@vals, $islocal); +} + +sub day_list { + shift if ref($_[0]) && $_[0]->isa(__PACKAGE__); + my @old = @DAY_LIST; + if (@_) { + @DAY_LIST = @_; + &Time::Piece::_default_locale(); + } + return @old; +} + +sub mon_list { + shift if ref($_[0]) && $_[0]->isa(__PACKAGE__); + my @old = @MON_LIST; + if (@_) { + @MON_LIST = @_; + &Time::Piece::_default_locale(); + } + return @old; +} + +sub time_separator { + shift if ref($_[0]) && $_[0]->isa(__PACKAGE__); + my $old = $TIME_SEP; + if (@_) { + $TIME_SEP = $_[0]; + } + return $old; +} + +sub date_separator { + shift if ref($_[0]) && $_[0]->isa(__PACKAGE__); + my $old = $DATE_SEP; + if (@_) { + $DATE_SEP = $_[0]; + } + return $old; +} + +use overload + '""' => \&cdate, + 'cmp' => \&str_compare, + 'fallback' => undef; + +sub cdate { + my $time = shift; + if ($time->[c_islocal]) { + return scalar(CORE::localtime($time->epoch)); + } + else { + return scalar(CORE::gmtime($time->epoch)); + } +} + +sub str_compare { + my ($lhs, $rhs, $reverse) = @_; + if (blessed($rhs) && $rhs->isa('Time::Piece')) { + $rhs = "$rhs"; + } + return $reverse ? $rhs cmp $lhs->cdate : $lhs->cdate cmp $rhs; +} + +use overload + '-' => \&subtract, + '+' => \&add; + +sub subtract { + my $time = shift; + my $rhs = shift; + + if (shift) { + return $rhs - "$time"; + } + + if (blessed($rhs) && $rhs->isa('Time::Piece')) { + return Time::Seconds->new($time->epoch - $rhs->epoch); + } + else { + return $time->_mktime(($time->epoch - $rhs), $time->[c_islocal]); + } +} + +sub add { + my $time = shift; + my $rhs = shift; + return $time->_mktime(($time->epoch + $rhs), $time->[c_islocal]); +} + +use overload + '<=>' => \&compare; + +sub get_epochs { + my ($lhs, $rhs, $reverse) = @_; + unless (blessed($rhs) && $rhs->isa('Time::Piece')) { + $rhs = $lhs->new($rhs); + } + if ($reverse) { + return $rhs->epoch, $lhs->epoch; + } + return $lhs->epoch, $rhs->epoch; +} + +sub compare { + my ($lhs, $rhs) = get_epochs(@_); + return $lhs <=> $rhs; +} + +sub add_months { + my ($time, $num_months) = @_; + + croak("add_months requires a number of months") unless defined($num_months); + + my $final_month = $time->_mon + $num_months; + my $num_years = 0; + + if ($final_month > 11 || $final_month < 0) { + if ($final_month < 0 && $final_month % 12 == 0) { + $num_years = int($final_month / 12) + 1; + } + else { + $num_years = int($final_month / 12); + } + $num_years-- if ($final_month < 0); + $final_month = $final_month % 12; + } + + my @vals = _mini_mktime($time->sec, $time->min, $time->hour, + $time->mday, $final_month, + $time->year - 1900 + $num_years); + + return scalar $time->_mktime(\@vals, $time->[c_islocal]); +} + +sub add_years { + my ($time, $years) = @_; + $time->add_months($years * 12); +} + +sub truncate { + my ($time, %params) = @_; + return $time unless exists $params{to}; + + my %units = ( + second => 0, + minute => 1, + hour => 2, + day => 3, + month => 4, + quarter => 5, + year => 5 + ); + + my $to = $units{$params{to}}; + croak "Invalid value of 'to' parameter: $params{to}" unless defined $to; + + my $start_month = 0; + if ($params{to} eq 'quarter') { + $start_month = int( $time->_mon / 3 ) * 3; + } + + my @down_to = (0, 0, 0, 1, $start_month, $time->year); + return $time->_mktime([@down_to[0..$to-1], @$time[$to..c_isdst]], $time->[c_islocal]); +} + +sub _translate_format { + my ( $format, $trans_map, $time ) = @_; + + $format =~ s/%%/\e\e/g; + + my $lexer = _build_format_lexer($format); + + while(my $flag = $lexer->() ){ + next unless exists $trans_map->{$flag}; + $format = $trans_map->{$flag}($format, $time); + } + + $format =~ s/\e\e/%%/g; + return $format; +} + +sub _build_format_lexer { + my $format = shift(); + return sub { + LABEL: { + return $1 if $format =~ m/\G%([a-zA-Z])/gc; + redo LABEL if $format =~ m/\G(.)/gc; + return; + } + }; +} + +sub use_locale { + my $locales = _get_localization(); + + if ( !$locales->{PM} || !$locales->{AM} || + ( $locales->{PM} eq $locales->{AM} ) ) { + $locales->{PM} = ''; + $locales->{AM} = ''; + } + + $locales->{pm} = lc $locales->{PM}; + $locales->{am} = lc $locales->{AM}; + + $locales->{c_fmt} = ''; + + if( @{$locales->{weekday}} < 7 ){ + @{$locales->{weekday}} = @FULLDAY_LIST; + } else { + @FULLDAY_LIST = @{$locales->{weekday}}; + } + + if( @{$locales->{wday}} < 7 ){ + @{$locales->{wday}} = @DAY_LIST; + } else { + @DAY_LIST = @{$locales->{wday}}; + } + + if( @{$locales->{month}} < 12 ){ + @{$locales->{month}} = @FULLMON_LIST; + } else { + @FULLMON_LIST = @{$locales->{month}}; + } + + if( @{$locales->{mon}} < 12 ){ + @{$locales->{mon}} = @MON_LIST; + } else { + @MON_LIST= @{$locales->{mon}}; + } + + $LOCALE = $locales; +} + +sub _default_locale { + my $locales = {}; + @{ $locales->{weekday} } = @FULLDAY_LIST; + @{ $locales->{wday} } = @DAY_LIST; + @{ $locales->{month} } = @FULLMON_LIST; + @{ $locales->{mon} } = @MON_LIST; + $locales->{alt_month} = $locales->{month}; + + $locales->{PM} = 'PM'; + $locales->{AM} = 'AM'; + $locales->{pm} = 'pm'; + $locales->{am} = 'am'; + $locales->{c_fmt} = ''; + + $LOCALE = $locales; +} + +sub _locale { + return $LOCALE; +} + +1; + +__END__ + +=head1 NAME + +Time::Piece - Object Oriented time objects + +=head1 SYNOPSIS + + use Time::Piece; + + my $t = localtime; + print "Time is $t\n"; + print "Year is ", $t->year, "\n"; + +=head1 DESCRIPTION + +This module replaces the standard localtime and gmtime functions with +implementations that return objects. It does so in a backwards compatible +manner, so that using localtime/gmtime in the way documented in perlfunc +will still return what you expect. + +This is a port of the CPAN Time::Piece module for PerlOnJava. + +=head1 AUTHOR + +Matt Sergeant, matt@sergeant.org + +Jarkko Hietaniemi, jhi@iki.fi (while creating Time::Piece for core perl) + +=head1 COPYRIGHT AND LICENSE + +Copyright 2001, Larry Wall. + +This module is free software, you may distribute it under the same terms +as Perl. + +=head1 SEE ALSO + +The excellent Calendar FAQ at L + +=cut diff --git a/src/main/perl/lib/Time/Seconds.pm b/src/main/perl/lib/Time/Seconds.pm new file mode 100644 index 000000000..647278670 --- /dev/null +++ b/src/main/perl/lib/Time/Seconds.pm @@ -0,0 +1,260 @@ +package Time::Seconds; + +use strict; +use warnings; + +our $VERSION = '1.3401'; + +use Exporter 5.57 'import'; + +our @EXPORT = qw( + ONE_MINUTE + ONE_HOUR + ONE_DAY + ONE_WEEK + ONE_MONTH + ONE_YEAR + ONE_FINANCIAL_MONTH + LEAP_YEAR + NON_LEAP_YEAR +); + +our @EXPORT_OK = qw(cs_sec cs_mon); + +use constant { + ONE_MINUTE => 60, + ONE_HOUR => 3_600, + ONE_DAY => 86_400, + ONE_WEEK => 604_800, + ONE_MONTH => 2_629_744, # ONE_YEAR / 12 + ONE_YEAR => 31_556_930, # 365.24225 days + ONE_FINANCIAL_MONTH => 2_592_000, # 30 days + LEAP_YEAR => 31_622_400, # 366 * ONE_DAY + NON_LEAP_YEAR => 31_536_000, # 365 * ONE_DAY + cs_sec => 0, + cs_mon => 1, +}; + +use overload + 'fallback' => 'undef', + '0+' => \&seconds, + '""' => \&seconds, + '<=>' => \&compare, + '+' => \&add, + '-' => \&subtract, + '-=' => \&subtract_from, + '+=' => \&add_to, + '=' => \© + +sub new { + my $class = shift; + my ($val) = @_; + $val = 0 unless defined $val; + bless \$val, $class; +} + +sub _get_ovlvals { + my ($lhs, $rhs, $reverse) = @_; + $lhs = $lhs->seconds; + + if (UNIVERSAL::isa($rhs, 'Time::Seconds')) { + $rhs = $rhs->seconds; + } + elsif (ref($rhs)) { + die "Can't use non Seconds object in operator overload"; + } + + if ($reverse) { + return $rhs, $lhs; + } + return $lhs, $rhs; +} + +sub compare { + my ($lhs, $rhs) = _get_ovlvals(@_); + return $lhs <=> $rhs; +} + +sub add { + my ($lhs, $rhs) = _get_ovlvals(@_); + return Time::Seconds->new($lhs + $rhs); +} + +sub add_to { + my $lhs = shift; + my $rhs = shift; + $rhs = $rhs->seconds if UNIVERSAL::isa($rhs, 'Time::Seconds'); + $$lhs += $rhs; + return $lhs; +} + +sub subtract { + my ($lhs, $rhs) = _get_ovlvals(@_); + return Time::Seconds->new($lhs - $rhs); +} + +sub subtract_from { + my $lhs = shift; + my $rhs = shift; + $rhs = $rhs->seconds if UNIVERSAL::isa($rhs, 'Time::Seconds'); + $$lhs -= $rhs; + return $lhs; +} + +sub copy { + Time::Seconds->new(${$_[0]}); +} + +sub seconds { + my $s = shift; + return $$s; +} + +sub minutes { + my $s = shift; + return $$s / 60; +} + +sub hours { + my $s = shift; + $s->minutes / 60; +} + +sub days { + my $s = shift; + $s->hours / 24; +} + +sub weeks { + my $s = shift; + $s->days / 7; +} + +sub months { + my $s = shift; + $s->days / 30.4368541; +} + +sub financial_months { + my $s = shift; + $s->days / 30; +} + +sub years { + my $s = shift; + $s->days / 365.24225; +} + +sub _counted_objects { + my ($n, $counted) = @_; + my $number = sprintf("%d", $n); + $counted .= 's' if 1 != $number; + return ($number, $counted); +} + +sub pretty { + my $s = shift; + my $str = ""; + + if ($s < 0) { + $s = -$s; + $str = "minus "; + } + + if ($s >= ONE_MINUTE) { + if ($s >= ONE_HOUR) { + if ($s >= ONE_DAY) { + my ($days, $sd) = _counted_objects($s->days, "day"); + $str .= "$days $sd, "; + $s -= ($days * ONE_DAY); + } + my ($hours, $sh) = _counted_objects($s->hours, "hour"); + $str .= "$hours $sh, "; + $s -= ($hours * ONE_HOUR); + } + my ($mins, $sm) = _counted_objects($s->minutes, "minute"); + $str .= "$mins $sm, "; + $s -= ($mins * ONE_MINUTE); + } + + $str .= join " ", _counted_objects($s->seconds, "second"); + + return $str; +} + +1; + +__END__ + +=head1 NAME + +Time::Seconds - a simple API to convert seconds to other date values + +=head1 SYNOPSIS + + use Time::Piece; + use Time::Seconds; + + my $t = localtime; + $t += ONE_DAY; + + my $t2 = localtime; + my $s = $t - $t2; + + print "Difference is: ", $s->days, "\n"; + +=head1 DESCRIPTION + +This module is part of the Time::Piece distribution. It allows the user +to find out the number of minutes, hours, days, weeks or years in a given +number of seconds. It is returned by Time::Piece when you delta two +Time::Piece objects. + +Time::Seconds also exports the following constants: + + ONE_DAY + ONE_WEEK + ONE_HOUR + ONE_MINUTE + ONE_MONTH + ONE_YEAR + ONE_FINANCIAL_MONTH + LEAP_YEAR + NON_LEAP_YEAR + +=head1 METHODS + +The following methods are available: + + my $val = Time::Seconds->new(SECONDS) + $val->seconds; + $val->minutes; + $val->hours; + $val->days; + $val->weeks; + $val->months; + $val->financial_months; # 30 days + $val->years; + $val->pretty; # gives English representation of the delta + +The usual arithmetic (+,-,+=,-=) is also available on the objects. + +The methods make the assumption that there are 24 hours in a day, 7 days +in a week, 365.24225 days in a year and 12 months in a year. + +=head1 AUTHOR + +Matt Sergeant, matt@sergeant.org + +Tobias Brox, tobiasb@tobiasb.funcom.com + +Balázs Szabó (dLux), dlux@kapu.hu + +=head1 COPYRIGHT AND LICENSE + +Copyright 2001, Larry Wall. + +This module is free software, you may distribute it under the same terms +as Perl. + +=cut