CGI/Perl Guide | Learning Center | Forums | Advertise | Login
Site Search: in

  Main Index MAIN
INDEX
Search Posts SEARCH
POSTS
Who's Online WHO'S
ONLINE
Log in LOG
IN

Home: Perl Programming Help: Intermediate:
Postage cost calculation

 



Zhris
Enthusiast

Jun 2, 2011, 3:23 PM

Post #1 of 11 (4621 views)
Postage cost calculation Can't Post

Hi everyone,

I was trying to look for some kind of UK (Royal Mail) postage API with no success, then looked for some kind of server side script to handle postage for me (Perl, PHP, ASP etc) also with no success. Therefore I decided I would write my own script from scratch. This also had the benefit of being able to use my own custom rates.

- My main issue is regarding my data, which I have seemed to over complicate, so when it comes to creating 1) an admin feature to update it, 2) an overview, to display in a neatly formatted table the postage rates for each location - I may have difficulty (any idiot must be able to use it). I tried to create the data to be as compact as possible i.e. using a multiplier system where consecutive rates are part of a "pattern".

- Another issue is my use of infinity, whats the point in even entering a system of loops, when the postage cost never changes. Maybe I should try to recognize when it gets to a stage like this, for efficiency purposes.

- Finally, I also have the issue that I have to account for the possibility of a huge weight input, and since theres a size limit (20000g national, 2000g international) I would need to repeat i.e. so orders can be split up into seperate packages. I'm sure I won't have difficulties doing this (maybe use an inner subroutine, or even try out the redo command), but before doing so I wanted some advice in improving my script.

If anyone has done something like this before, or has any advice, could they please help me out a little. Maybe a pointer to a free quality pre-written script. Or just some feedback on my script. By the way, modules are out of the question, since i'm unable to install them onto my web server.

Here is what I have come up with so far, it still needs a little work in its current state (ignore my questionable cost operations, I was just playing around):


Code
#! /usr/bin/perl 
use strict;
use CGI::Carp qw/fatalsToBrowser warningsToBrowser/;
print "content-type: text/plain\n\n";
warningsToBrowser(1);

###########################################################

my $weight = "800";

my $postage = Postage($weight);

while (my ($location, $cost) = (each %{$postage})) {
print "$location => $cost\n";
}

###########################################################

sub Postage {
my $weight = shift;
die "Invalid weight" unless ($weight < "1000000000");

my $d = '(\d+(\.\d+)?|Inf)';
my %costoperations = (
'=' => sub { $_[1] },
'+' => sub { $_[0] + $_[1] },
'=*' => sub { $_[0] * $_[1] },
'+*' => sub { $_[0] + ($_[0] * $_[1]) },
'=' => sub { $_[0] / $_[1] },
'+' => sub { $_[0] + ($_[0] / $_[1]) }
);
my %postage;

while (<DATA>) {
next if ($_ =~ m/^(\!|\s)/);

my @row = split(/\|/, $_);
my $service = $row[1];
my @rates = split(/\//, $row[2]);

my $totalcost = "0.00";
my $flag = 0;

lastmarker:
foreach my $rate (@rates) {
die "Invalid rate" unless ($rate =~ m/^ $d [>] $d (\=|\+|\=\*|\+\*|\=\|\+\) $d ([*]$d)? $/x);
my ($minweight, $minweightdecimal, $uptoweight, $uptoweightdecimal, $costoperator, $cost, $costdecimal, $multiplierwithoperator, $multiplier, $multiplierdecimal) = ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10);
$multiplier = $multiplier || 1;

for (my $i = 1; $i <= $multiplier; $i++) {
$totalcost = $costoperations{$costoperator}->($totalcost, $cost);

if (($weight >= $minweight) && ($weight <= ($minweight + $uptoweight))) {
$flag = 1;
last lastmarker;
}

$minweight += $uptoweight + 1;
}
}

$postage{$service} = ($flag) ? sprintf("%.2f", $totalcost) : undef;
}

return \%postage;
}

###########################################################

__DATA__
|UK & Europe|1>Inf=0.00|
|USA|1>199=2.00/201>199+1.00*Inf|
|Australia & New Zealand|1>199=2.00/201>199+1.1*10|
|Other|1>199=2.00/201>199+1.00*Inf|
|Test|1>99=1.00/101>99+0.90/201>99+0.80|


Thank you,

Chris


(This post was edited by Zhris on Jun 2, 2011, 3:24 PM)


miller
User

Jun 6, 2011, 3:25 PM

Post #2 of 11 (4565 views)
Re: [Zhris] Postage cost calculation [In reply to] Can't Post

Hi Chris,

I'd like to be able to help you, but unfortunately it's rather impossible to decipher what your intent is by your code or your description.

It'd also be helpful if the code didn't kill the wrapping of your description, and that would be helped if you didn't use the list assignment when saving your regex captures. It's rather difficult to read what is assigned where when you use that form, so I'd strongly advise that you rewrite your code as the following:


Code
		foreach my $rate (@rates) { 
my $operations = join '|', map {quotemeta} qw(= + =* +* = +);
die "Invalid rate" unless $rate =~ m{^$d [>] $d ($operations) $d ([*]$d)? $}x;
my $minweight = $1;
my $minweightdecimal = $2;
my $uptoweight = $3;
my $uptoweightdecimal = $4;
my $costoperator = $5;
my $cost = $6;
my $costdecimal = $7;
my $multiplierwithoperator = $8;
my $multiplier = $9 || 1;
my $multiplierdecimal = $10;

for my $i (1..$multiplier) {


Also, I'd llike to point out that you are assigning 10 saved captures when your regex only has 2. Not sure what that's about, but obviously there's a big there somewhere.

If you can describe what you want more clearly, I or someone would probably be able to help you.

Will await further information,
- Miller


Zhris
Enthusiast

Jun 7, 2011, 3:12 PM

Post #3 of 11 (4559 views)
Re: [miller] Postage cost calculation [In reply to] Can't Post

Hi Miller,

To put it simply, I need to write a function that will allow me to calculate the postage cost based on the weight of an order, using my own custom rates. Since I was unable to find anything suitable by searching, I started to write my own function, but I think i'm probably going the wrong way about it. Maybe some advice how somebody else would go about it is what i'm after. My original post was kind of a bunch of my thoughts/notes so I apologise if it wasn't clear enough.

I also apologise for my style of coding, its how i've always written it, and my brain is good at reading it like that too. To be honest I never knew why my message sometimes didn't wrap. Also, you'll see that $d contains a part of a regex for a "decimal" number, which accounts for the 8 "missing" captures.

I'm on holiday at the moment, but I planned on just continuing as I was, attempting to refine my function until it does everything I require, and not worry about the format of the data. But surely this is something many ecommerce developers have had to produce, and i'm here re-inventing the wheel.

Thanks for your reply,

Chris


miller
User

Jun 8, 2011, 3:58 PM

Post #4 of 11 (4546 views)
Re: [Zhris] Postage cost calculation [In reply to] Can't Post

Hi Chris,

I very much want to help you, but I would need you to explain in words what the rules you're trying to accomplish for calculating the postage. I believe that I have your code mostly deciphered now that you explained about the $d, and I definitely have a lot of things I could say about the approach.

Nevertheless, I'm going to focus on some smaller issues right now. Your delimiters for the data is very much not doing you any favors. Obviously this is a custom job, so you can do what you want. But it makes since to use a format that makes the code easier to read.

To that end, I suggest you use paragraph mode and delimit the different sections with double returns. This lets you list the different ranges one after another in a much easier to read format.

Additionally, I would strongly suggest that you redefine your ranges as something like "$minvalue to $maxvalue" instead of having them be include a length of the range and using the greater sign in a weird way.

Finally, if you don't want a specific grouping in a regex to captuer anything, use (?: ... ) instead of ( ... ). You had 3 different areas where you were saving teh decimal of a number only because you wanted it to be optional.

Anyway, I've only exercised some of these changes in the following bit of code. Hopefully, some of the refactoring will be helpful to you:


Code
#! /usr/bin/perl 

use CGI::Carp qw(fatalsToBrowser warningsToBrowser);

use strict;
use warnings;

print "Content-Type: text/plain\n\n";
warningsToBrowser(1);

###########################################################

my $weight = 800;

my $postage = Postage($weight);

while (my ($location, $cost) = (each %{$postage})) {
print "$location => $cost\n";
}

###########################################################

sub Postage {
my $weight = shift;
die "Invalid weight" unless ($weight < "1000000000");

my %costoperations = (
'=' => sub { $_[1] },
'+' => sub { $_[0] + $_[1] },
'=*' => sub { $_[0] * $_[1] },
'+*' => sub { $_[0] + ($_[0] * $_[1]) },
'=' => sub { $_[0] / $_[1] },
'+' => sub { $_[0] + ($_[0] / $_[1]) }
);
my %postage;

local $/ = "\n\n"; # Paragraph mode

RATE:
while (<DATA>) {
chomp;

# Comments begin with !
# All beginning space is ignored
my ($service, @rates) = grep {!/^!/} map {s/^\s+//; $_} split "\n";

$postage{$service} = '';

my $totalcost = 0;

for my $rate (@rates) {
# Build the Regex
my $d = '(\d+(?:\.\d+)?|Inf)';
my $operations = join '|', map {quotemeta} keys %costoperations;

$rate =~ m{^ $d [>] $d ($operations) $d ([*]$d)? $}x
or die "Invalid rate";

my $minweight = $1;
my $uptoweight = $2;
my $costoperator = $3;
my $cost = $4;
my $multiplierwithoperator = $5;
my $multiplier = $6 // 1;

for my $i (1..$multiplier) {
$totalcost = $costoperations{$costoperator}->($totalcost, $cost);

if (($weight >= $minweight) && ($weight <= ($minweight + $uptoweight))) {
$postage{$service} = sprintf "%.2f", $totalcost;
next RATE;
}

$minweight += $uptoweight + 1;
}
}
}

return \%postage;
}

###########################################################

__DATA__
UK & Europe
1>Inf=0.00

USA
1>199=2.00
201>199+1.00*Inf

Australia & New Zealand
1>199=2.00
201>199+1.1*10

Other
1>199=2.00
201>199+1.00*Inf

Test
1>99=1.00
101>99+0.90
201>99+0.80


If you describe what exactly the rules for postage are that you're trying to implement, then I could most likely advise you a lot better.

Good luck,
- Miller


Zhris
Enthusiast

Jun 9, 2011, 5:52 PM

Post #5 of 11 (4537 views)
Re: [miller] Postage cost calculation [In reply to] Can't Post

Dear Miller.

Im using a mobile phone to reply just to say that ill be back tomorrow and will be able to look through your reply properly.

Thanks a lot

Chris


Zhris
Enthusiast

Jun 10, 2011, 3:43 PM

Post #6 of 11 (4506 views)
Re: [Zhris] Postage cost calculation [In reply to] Can't Post

Hi Miller,

Thanks very much for all your effort. I've looked through your code and I like the improvements that you have made.

The new data format is much nicer. As this function will become part of a few other existing functions, I had originally wanted to keep the data in the same format as those. Since no one would ever need to read the data in its raw format, I didn't feel it was neccessary to make it readable. But I much prefer your way, as you say, it makes sense.

I did originally use ranges i.e. "1-200, 201-400, 401-600" instead of a "length", but this would mean having to use a whole other level of error checking i.e. check the right side is higher than the left side etc (me attempting to be efficient). I would very much like to go back to ranges.

I haven't delved enough into regular expressions yet, so thank you for explaining how to not capture a particular group. I never knew it was possible.

By the way, did you mean (never come across the // op)?:

Code
my $multiplier             = $6 // 1; 
my $multiplier = $6 || 1;


At this time, the rates are fairly basic:

UK = free all the time.
Europe = See http://www.royalmail.com/portal/rm/content1?mediaId=53800712&catId=400036 (top)
Rest of the world = See http://www.royalmail.com/portal/rm/content1?mediaId=53800712&catId=400036 (bottom)

"Cost operations" allow me to most importantly make delivery free after a certain weight i.e. Europe over 10000g will be 0.00.

The most important part, which I hadn't yet got round to doing is what I explained previously, since the maximum weight for i.e. Europe is 2000g, I would need the rates to "repeat" so that i.e. order = 3000g (above the max), first package = 2000g, second package = 1000g. I planned on doing this with some kind of $weightoffset, which will restart while appending to the existing $totalcost after i.e. 2000g. I hope I have explained this properly.

Anyhow, I will continue work on your refined function tomorrow. And I would massively appreciate any help or advice as you have done so already, its very helpful to see how others refine what i've done.

Thanks once again,

Chris


(This post was edited by Zhris on Jun 10, 2011, 4:25 PM)


miller
User

Jun 20, 2011, 5:44 PM

Post #7 of 11 (4419 views)
Re: [Zhris] Postage cost calculation [In reply to] Can't Post

The following is meant to be a proof of concept, and not a finished product.

Basically, I would suggest that you remove the need for a special markup language from your script entirely. It needless complicates the code and makes it much harder to use in my opinion. Take advantage of the fact that you know perl and code your more complicated equations in perl as well.

I still left the below with the configuration info in the __DATA__ block, but that's not strictly necessary either. You could just as easily hardcode the config data directly in a hash that would remove the need for parsing.


Code
#! /usr/bin/perl 

use CGI::Carp qw(fatalsToBrowser warningsToBrowser);
use POSIX qw(ceil);

use strict;
use warnings;

print "Content-Type: text/plain\n\n";
warningsToBrowser(1);

###########################################################

my $weight = 800;

my %postage = Postage($weight);

while (my ($location, $cost_ref) = each %postage) {
print "$location\n";
print "\t@$_\n" for @$cost_ref;
}

###########################################################

our %POSTAGE;
INIT {
local $/ = "\n\n"; # Paragraph mode

while (<DATA>) {
chomp;

# Comments begin with #
my ($service, @rates) = grep {!/^#/} split "\n";

# Eval rates
for (@rates) {
$_ = [split "\t"];
$_->[1] = eval $_->[1];
die "$service - $@" if $@;
}

$POSTAGE{$service} = \@rates;
}
}

sub Postage {
my $weight = shift;
die "Invalid weight" unless $weight < 1_000_000_000;

my %postage;

for my $service (sort keys %POSTAGE) {
# The Last Max
my $max = $POSTAGE{$service}[-1][0];

# Subdivide Package according to max weight
my @weights = $weight;
while ($max && $weights[-1] > $max) {
unshift @weights, $max;
$weights[-1] -= $max;
}

for my $w (@weights) {
for my $rate (@{$POSTAGE{$service}}) {
next if $rate->[0] && $w > $rate->[0];
push @{$postage{$service}}, [$w, sprintf "%.2f", $rate->[1]($w)];
last;
}
}
}

return %postage;
}

###########################################################

# Each section is __DATA__ is labelled by a destination
# and then by max weight and the sub that determines the cost.
# If no max weight is specified, then treat as infinate.

__DATA__
UK & Europe
sub { 0.00 }

USA
200 sub { 2.00 }
sub { my $w = shift; 2.00 + 1.00 * ceil(($w-200)/100) }

Australia & New Zealand
200 sub { 2.00 }
2200 sub { my $w = shift; 2.00 * (1 + 1 / 1.1) ** ceil(($w - 200) / 200) }

Other
200 sub { 2.00 }
sub { my $w = shift; 2.00 + 1.00 * ceil(($w - 200) / 200) }

Test
100 sub { 1.00 }
200 sub { 1.00 + 0.99 }
300 sub { 1.00 + 0.99 + 0.80 }


- Miller


(This post was edited by miller on Jun 20, 2011, 5:45 PM)
Attachments: postage.pl (2.07 KB)


miller
User

Jun 20, 2011, 5:49 PM

Post #8 of 11 (4417 views)
Re: [Zhris] Postage cost calculation [In reply to] Can't Post


In Reply To
By the way, did you mean (never come across the // op)?:

Code
my $multiplier             = $6 // 1; 
my $multiplier = $6 || 1;



The // operator was introduced in perl 5.10 I believe and is similar to || except that it test for being defined instead of truth. It's equivalent to the following:


Code
my $multiplier = defined $6 ? $6 : 1;


Where the || operator is truth


Code
my $multiplier = $6 ? $6 : 1;


- Miller


mors
New User

Jun 21, 2011, 2:15 AM

Post #9 of 11 (4402 views)
Re: [miller] Postage cost calculation [In reply to] Can't Post


Code
my $multiplier = ( $6 || 1 );



miller
User

Jun 21, 2011, 11:43 AM

Post #10 of 11 (4358 views)
Re: [mors] Postage cost calculation [In reply to] Can't Post


In Reply To

Code
my $multiplier = ( $6 || 1 );



And the point of that reply is what, mors? :)

- M


Zhris
Enthusiast

Aug 15, 2011, 12:56 PM

Post #11 of 11 (3248 views)
Re: [miller] Postage cost calculation [In reply to] Can't Post

Dear Miller,

Firstly, my apologies for bumping this post back up. A couple of months ago, without going into too much detail, there were a few personal issues in my life,
and I was forced to abandon my efforts on this project as well as others. On the upside, I kick started my career a couple of weeks ago, and started my first ever full time job
as a junior Perl software/web application developer, which is something I have dreamed of becoming since I was 14. Its going pretty well, although one of my first tasks
involves parsing largely variable XML structures into a standardized Perl data structure, which is proving to be tricky, but my use of XPath seems to be successful so far!

Anyway, I just wanted to let you know that your help was invaluable and I was able to use your code examples to produce a working "postage cost" function (although there are
still a couple things I need to adapt before implementing it live). Your efforts will/have played a huge role in increasing worldwide sales through one of my ecommerce websites.

Thank you very much, and my apologies once again for my delayed response.

Chris

 
 


Search for (options) Powered by Gossamer Forum v.1.2.0

Web Applications & Managed Hosting Powered by Gossamer Threads
Visit our Mailing List Archives