Skip to content

Instantly share code, notes, and snippets.

@LukeSavefrogs
Last active February 21, 2025 14:32
Show Gist options
  • Save LukeSavefrogs/c41a5193fef11fc426b9acd9adb63917 to your computer and use it in GitHub Desktop.
Save LukeSavefrogs/c41a5193fef11fc426b9acd9adb63917 to your computer and use it in GitHub Desktop.
Perl Text template formatters
# ------------------------------------------------------------------------------
# Text::Template
# ------------------------------------------------------------------------------
# This module provides a simple way to perform Python-like string formatting
# using a dictionary of key-value pairs. The template string should contain
# placeholders in the form of '%(key)s'.
#
package Text::Template;
use v5.16;
use strict;
use warnings;
use English qw(-no_match_vars);
# Perform a Python-like string formatting using a dictionary of key-value pairs.
# The template string should contain placeholders in the form of '%(key)s'.
#
# Parameters:
# $template: The template string to format
# $data: The dictionary of key-value pairs to use for the formatting
# $config: Optional configuration hash
# - throwOnMissingKey: If true, an exception is thrown if a key is not found in the data dictionary
# - defaultValue: The default value to use if the key is not found in the data dictionary
# - formatRegex: The regex pattern to use for the placeholders
#
# Returns:
# The formatted string
#
# Example:
# my $template = Text::Template->new(template => "test1:%(test1)s test2:%(test2)s");
# my $result = $template->format({ test1 => 10 });
# print($result); # Output: test1:10 test2:''
#
sub new {
my ($class, %args) = @_;
my $self = {
template => $args{template} // "",
config => {
throwOnMissingKey => 0,
defaultValue => "",
formatRegex => '%\((?<key>[^)]+)\)s',
%{ $args{config} // {} },
},
};
bless $self, $class;
return $self;
}
# Format the template string using the provided data dictionary.
#
# Parameters:
# $data: The dictionary of key-value pairs to use for the formatting
#
# Returns:
# The formatted string
#
# Example:
# my $result = $template->format({ test1 => 10 });
# print($result); # Output: test1:10 test2:''
#
sub format {
my ($self, $data) = @_;
my $result = $self->{template};
my $config = $self->{config};
while ($result =~ m/$config->{formatRegex}/g) {
die "Unexpected number of capture groups found in the format regex (expected 1, found $#+).\n"
if ($#+ != 1);
my $key = $1;
if (!exists $data->{$key} && $config->{throwOnMissingKey}) {
die "Missing key '$key' in the data dictionary.\n";
}
my $value = $data->{$key} // $config->{defaultValue};
substr($result, $-[0], $+[0] - $-[0], $value);
}
return $result;
}
# Get the template string.
#
sub get_template {
my ($self) = @_;
return $self->{template};
}
# Get the configuration hash.
#
sub get_config {
my ($self) = @_;
return $self->{config};
}
# Get the groups found in the template string.
#
# Example:
# my %groups = $template->get_groups();
# print(Dumper(\%groups)); # Output: { 'test1' => '%(test1)s', 'test2' => '%(test2)s' }
#
sub get_groups {
my ($self) = @_;
my %groups = ();
while ($self->{template} =~ m/(?<full>$self->{config}->{formatRegex})/g) {
die "Unexpected number of capture groups found in the format regex (expected 2, found $#+).\n"
if ($#+ != 2);
my $key = $+{key} // $2;
$groups{$key} = $+{full};
}
return %groups;
}
# https://stackoverflow.com/a/3395759/8965861
__PACKAGE__->run( @ARGV ) unless caller;
sub run {
my( $class, @args ) = @_;
use Data::Dumper;
use Test::More;
ok(
Text::Template->new(template => "test1:%(test1)s test2:%(test2)s")->get_template() eq "test1:%(test1)s test2:%(test2)s",
"Retrieve template"
);
ok(
Text::Template->new(template => "test1:%(test1)s test2:%(test2)s")->get_config()->{throwOnMissingKey} == 0,
"Default configuration value: 'throwOnMissingKey'"
);
ok(
Text::Template->new(template => "test1:%(test1)s test2:%(test2)s")->get_config()->{defaultValue} eq "",
"Default configuration value: 'defaultValue'"
);
ok(
Text::Template->new(template => "test1:%(test1)s test2:%(test2)s")->get_config()->{formatRegex} eq '%\((?<key>[^)]+)\)s',
"Default configuration value: 'formatRegex'"
);
my %groups = Text::Template->new(template => "test1:%(test1)s test2:%(test2)s")->get_groups();
is_deeply(
\%groups,
{ test1 => '%(test1)s', test2 => '%(test2)s' },
"Get template capturing groups"
);
ok(
Text::Template->new(template => "test1:%(test1)s test2:%(test2)s")->format({ test1 => 10 }) eq "test1:10 test2:",
"Missing key"
);
ok(
Text::Template->new(template => "test1:%(test1)s test2:%(test2)s")->format({ test1 => 10, test2 => 20 }) eq "test1:10 test2:20",
"All keys"
);
ok(
Text::Template->new(template => "test1:%(test1)s test2:%(test2)s", config => { defaultValue => "N/A" })->format({ test1 => 10 }) eq "test1:10 test2:N/A",
"Default value"
);
ok(
Text::Template->new(template => "test1:{{test1}} test2:{{test2}}", config => { formatRegex => '\{\{(?<key>[^}]+)\}\}' })->format({ test1 => 10 }) eq "test1:10 test2:",
"Custom format regex"
);
eval {
Text::Template->new(template => "test1:%(test1)s test2:%(test2)s", config => { throwOnMissingKey => 1 })->format({ test1 => 10 });
fail("Expected an exception to be thrown.");
1;
} or do {
my $eval_error = $@ || "error";
ok($eval_error eq "Missing key 'test2' in the data dictionary.\n", "Expect exception on missing key: $eval_error");
};
done_testing();
}
1;
use v5.16;
use strict;
use warnings;
use English qw(-no_match_vars);
# Perform a Python-like string formatting using a dictionary of key-value pairs.
# The template string should contain placeholders in the form of '%(key)s'.
#
# Parameters:
# $template: The template string to format
# $data: The dictionary of key-value pairs to use for the formatting
# $config: Optional configuration hash
# - throwOnMissingKey: If true, an exception is thrown if a key is not found in the data dictionary
# - defaultValue: The default value to use if the key is not found in the data dictionary
# - formatRegex: The regex pattern to use for the placeholders
#
# Returns:
# The formatted string
#
# Example:
# my $result = formatter("test1:%(test1)s test2:'%(test2)s'", { test1 => 10 });
# print($result); # Output: test1:10 test2:''
#
sub formatter {
my ($template, $data, $config) = @_;
die "Missing mandatory 'template' parameter (type=scalar).\n" if (!defined $template);
die "Missing mandatory 'data' parameter (type=hashref).\n" if (!defined $data);
if (ref($data) ne "HASH") {
die "Invalid 'data' parameter (type=" . ref($data) . "). Expected type: hashref.\n";
}
if (defined $config && ref($config) ne "HASH") {
die "Invalid 'config' parameter (type=" . ref($config) . "). Expected type: hashref.\n";
}
my $result = $template;
$config = {
throwOnMissingKey => 0,
defaultValue => "",
formatRegex => '%\((?<key>[^)]+)\)s',
%{ $config // {} },
};
while ($result =~ m/$config->{formatRegex}/g) {
# print("Capture groups: $#+\n");
die "Unexpected number of capture groups found in the format regex (expected 1, found $#+).\n"
if ($#+ != 1);
my $key = $1;
if (!exists $data->{$key} && $config->{throwOnMissingKey}) {
die "Missing key '$key' in the data dictionary.\n";
}
my $value = $data->{$key} // $config->{defaultValue};
substr($result, $-[0], $+[0] - $-[0], $value);
}
return $result;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment