perl.gg / hidden-gems

<!-- category: hidden-gems -->

File Age in Fractional Days

2026-05-06

How old is this file? The hard way:
my @stat = stat('script.pl'); my $age_seconds = time() - $stat[9]; my $age_days = $age_seconds / 86400;
The Perl way:
my $age_days = -M 'script.pl';
One operator. No stat call. No date math. No dividing by 86400. Just -M and a filename. It returns the file's age in fractional days since your script started running.

Was the file modified in the last hour? -M $file < (1/24). In the last 5 minutes? -M $file < (5/1440). Older than a week? -M $file > 7.

This is one of Perl's file test operators, and it's criminally underused.

Part 1: WHAT -M RETURNS

-M returns the number of days (as a floating-point number) since the file was last modified, measured from when your script started running.
my $age = -M '/var/log/syslog'; say $age; # something like 0.0834 (about 2 hours)
The reference point is $^T, the time the script began. This means every -M call in the same script uses the same baseline. Files don't appear to age during execution.
VALUE MEANING -------- ---------------------------- 0.0007 modified ~1 minute ago 0.0417 modified ~1 hour ago 0.5 modified 12 hours ago 1.0 modified exactly 1 day ago 7.0 modified 1 week ago 30.5 modified about a month ago
Fractional days sound weird, but they make comparisons trivial. "Less than 1" means today. "Greater than 7" means more than a week old. No unit conversion needed.

Part 2: -A AND -C

-M has two siblings:
my $mod_age = -M $file; # modification time (mtime) my $access_age = -A $file; # access time (atime) my $change_age = -C $file; # inode change time (ctime)
-M is when the file's content was last changed. The one you almost always want.

-A is when the file was last read. Useful for finding files nobody has touched in a while. Note: many filesystems mount with noatime for performance, which makes -A unreliable.

-C is when the file's metadata changed. Permissions, ownership, hard links. On Unix, this is the inode change time. It's not "creation time" even though the letter suggests it.

# find files that were read more recently than they were modified # (someone looked at it but didn't edit it) if (-A $file < -M $file) { say "$file was read since its last edit"; }

Part 3: PRACTICAL AGE CHECKS

The real power is in simple comparisons. No date parsing. No DateTime objects. Just numbers.

Modified today (within last 24 hours):

if (-M $file < 1) { say "$file was modified today"; }
Modified in the last hour:
if (-M $file < 1/24) { say "$file was modified in the last hour"; }
Modified in the last 5 minutes:
if (-M $file < 5/1440) { say "$file is fresh (< 5 min)"; }
Older than 30 days:
if (-M $file > 30) { say "$file is stale"; }
Between 1 and 7 days old:
my $age = -M $file; if ($age >= 1 && $age <= 7) { say "$file was modified this week"; }
No stat(). No localtime(). No POSIX::strftime(). Just a number and a comparison.

Part 4: CACHE INVALIDATION

Here's where -M shines in real code. You have a cache file. If it's fresh enough, use it. If it's stale, rebuild it.
my $cache = '/tmp/myapp_cache.json'; if (-e $cache && -M $cache < 1/24) { # cache is less than 1 hour old, use it open my $fh, '<', $cache or die "Cannot read $cache: $!\n"; my $data = do { local $/; <$fh> }; close $fh; return decode_json($data); } else { # cache is stale or missing, rebuild my $data = fetch_fresh_data(); open my $fh, '>', $cache or die "Cannot write $cache: $!\n"; print $fh encode_json($data); close $fh; return $data; }
Two lines of cache logic: -e $cache && -M $cache < 1/24. That's the entire invalidation strategy. No timestamp columns. No TTL calculations. The filesystem is the clock.

Part 5: LOG ROTATION CHECKS

Sysadmin scripts love -M. Check if logs need rotation:
#!/usr/bin/env perl use strict; use warnings; use feature 'say'; my @logs = glob('/var/log/myapp/*.log'); for my $log (@logs) { my $age = -M $log; my $size = -s $log; if ($age > 7 || $size > 100_000_000) { say sprintf("ROTATE: %-40s age=%.1f days size=%d MB", $log, $age, $size / 1_000_000); } }
That walks a log directory and flags anything older than a week or bigger than 100 MB. The -M and -s operators make the logic almost pseudocode.

Part 6: FILE TEST STACKING AND THE _ CACHE

Perl caches the result of the last file test in a special filehandle called _ (underscore). You can chain tests without hitting the filesystem multiple times:
if (-f $file && -r _ && -M _ < 1) { say "$file is a regular, readable file modified today"; }
The first test -f $file does the actual stat() call and caches the result. The subsequent -r _ and -M _ reuse that cached stat data. Three tests, one system call.

This matters when you're scanning thousands of files:

my @recent_readable = grep { -f $_ && -r _ && -M _ < 7 } @files;
Without the _ cache, that's three stat() calls per file. With it, one.

Starting in Perl 5.10, you can also stack file tests directly:

if (-f -r -w $file) { say "$file is a regular, readable, writable file"; }
But -M can't participate in this stacked syntax because it returns a number, not a boolean. You still need to use _ for combining -M with other tests:
if (-f $file && -M _ < 1) { say "regular file, modified today"; }

Part 7: FINDING RECENT FILES

Scan a directory for files modified in the last N days:
sub find_recent { my ($dir, $max_age_days) = @_; $max_age_days //= 1; opendir my $dh, $dir or die "Cannot open $dir: $!\n"; my @recent; while (my $entry = readdir $dh) { next if $entry =~ m~^\.~; my $path = "$dir/$entry"; next unless -f $path; push @recent, $path if -M _ < $max_age_days; } closedir $dh; return sort { (-M $a) <=> (-M $b) } @recent; } my @today = find_recent('/var/log', 1); say $_ for @today;
The sort at the end orders by age, most recently modified first. -M in a sort comparator is perfectly valid.

Part 8: COMPARISON TO STAT()

Here's the same logic with stat():
# with stat() my $mtime = (stat $file)[9]; my $age_days = (time() - $mtime) / 86400; if ($age_days < 1) { ... } # with -M if (-M $file < 1) { ... }
The stat() version is five tokens. The -M version is five tokens too, but it's one expression instead of three statements. No intermediate variables. No magic number 86400.

There is one important difference. -M measures from $^T (script start time), not from time() (current time). For short-running scripts, the difference is negligible. For long-running daemons, it matters.

If you need age relative to the current moment in a long-running process:

# reset the baseline to now $^T = time(); my $age = -M $file; # now measures from this moment
Or just use stat():
my $age = (time() - (stat $file)[9]) / 86400;
For scripts that run and exit (the vast majority), -M is simpler and cleaner.

Part 9: CLEANUP SCRIPTS

The classic sysadmin pattern. Delete temp files older than N days:
#!/usr/bin/env perl use strict; use warnings; use feature 'say'; my $dir = '/tmp/myapp'; my $max_age = 7; # days my $dry_run = 1; opendir my $dh, $dir or die "Cannot open $dir: $!\n"; while (my $file = readdir $dh) { my $path = "$dir/$file"; next unless -f $path; if (-M _ > $max_age) { if ($dry_run) { say sprintf("WOULD DELETE: %s (%.1f days old)", $path, -M _); } else { unlink $path or warn "Cannot delete $path: $!\n"; say "DELETED: $path"; } } } closedir $dh;
The -M _ > $max_age is the entire decision. The _ reuses the stat from the -f test above it. Clean, efficient, obvious.

Part 10: NEGATIVE VALUES

Here's a weird one. -M can return negative numbers:
# touch a file with a future timestamp system('touch', '-t', '203001010000', '/tmp/future.txt'); say -M '/tmp/future.txt'; # negative! (file is from "the future")
A negative value means the file's modification time is after $^T. This can happen with:

It's rare, but if you're doing automated age checks, guard against it:

my $age = -M $file; if ($age >= 0 && $age < 1) { say "Modified today"; }
That >= 0 protects you from future-dated files sneaking through.
.--. |o_o | "stat() is 10 characters. |:_/ | -M is 2." // \ \ (| | ) /'\_ _/`\ \___)=(___/ -M modification age -A access age -C inode change age Returns fractional days. 1 = yesterday. 0.5 = 12 hours ago. 7 = a week.
-M is one of those operators that makes you realize how much Perl was designed by someone who actually wrote sysadmin scripts. Checking file age shouldn't require three lines of code and a division by 86400. It should be two characters and a comparison.

File age in days. No imports. No date math. No stat unpacking. Just -M and a number. If you're writing any script that touches the filesystem, this operator will save you time on every single file test.

perl.gg