Beefy Boxes and Bandwidth Generously Provided by pair Networks
The stupid question is the question not asked
 
PerlMonks  

Multidimensional arrays

by Bod (Parson)
on Jun 06, 2023 at 14:55 UTC ( [id://11152656]=perlquestion: print w/replies, xml ) Need Help??

Bod has asked for the wisdom of the Perl Monks concerning the following question:

We operate a few sites with subscriptions. Until recently, the subscription cost has been fixed with just three plan options - monthly, 6 monthly or annually. However, we now want to increase the cost on one site for new members but not existing members. Existing members will have their current subscription rate locked for life.

Currently, the three subscription rates are declared in a parameters file:

our $stripe_price_1 = 'price_123245'; our $stripe_price_6 = 'price_123267'; our $stripe_price_12 = 'price_123289';
The price code is passed to Stripe during the checkout process.

Having multiple prices in the future means we need to adopt a different approach. Users have the option to change between payment plans so they can swap from monthly or annually, etc. Therefore, we need to know which pricing level they are on as well as the plan. We can (and currently do) easily get the plan from Stripe. But getting the level is more tricky. We'd have to get the price code and work backwards.

So, I am thinking of adding two fields to the Subscription table of the database - priceLevel and pricePlan - and replacing the above declarations with a two dimensional Perl array of hashref:

my @price; $price[1,1] = { 'name' => 'Early Adopter', 'plan' => 1, 'length' => 1, 'price' => 3.49, 'stripe' => 'price_123245', }; $price[1,6] = { 'name' => 'Early Adopter', 'plan' => 1, 'length' => 6, 'price' => 18.99, 'stripe' => 'price_123267', }; $price[1,12] = { 'name' => 'Early Adopter', 'plan' => 1, 'length' => 12, 'price' => 32.49, 'stripe' => 'price_123289', }; $price[2,1] = { 'name' => 'First Increase', 'plan' => 2, 'length' => 1, 'price' => 3.99, 'stripe' => 'price_123445', }; $price[2,6] = { ... }; $price[2,12] = { 'name' => 'First Increase', 'plan' => 2, 'length' => 12, 'price' => 33.99, 'stripe' => 'price_123489', }; $price[3,1] = { ... }; $price[3,6] = { ... }; $price[3,12] = { ... };
The two dimensions of the array corresponding to the two newly added database fields.

I have two doubts about this approach...

  1. There are lots of 'spaces' in the array
  2. I seem to recall reading (probably) in The Monastry that multidimensional arrays are not a good idea

Any advice very welcome...

Replies are listed 'Best First'.
Re: Multidimensional arrays
by choroba (Cardinal) on Jun 06, 2023 at 15:22 UTC
    Why do you want to create a sparse array? You don't want to iterate over it, you want to retrieve the details for a given plan. Use a hash of hashes instead.

    Also, you probably don't need to repeat the "plan" and "length" in the inner hash, you already know them as they're the keys to it (unless you need to iterate over the values somewhere).

    my %price = ( 1 => { 1 => { name => 'Early Adopter', price => 3.49, stripe => 'price_123245', }, 6 => { name => 'Early Adopter', price => 18.99, stripe => 'price_123267', }, 12 => { name => 'Early Adopter', price => 32.49, stripe => 'price_123289', }}, 2 => { 1 => { name => 'First Increase', price => 3.99, stripe => 'price_123445', }, ...
    Update: BTW, the syntax you used is not correct.
    Multidimensional syntax $price[1,1] not supported at 1.pl line 6.

    The correct syntax is $price[1][1].

    Similarly, the syntax for the hash of hashes will be

    my $gbp_sum = $price{$plan}{$length}{price};

    map{substr$_->[0],$_->[1]||0,1}[\*||{},3],[[]],[ref qr-1,-,-1],[{}],[sub{}^*ARGV,3]

      Thanks choroba

      The correct syntax is $price[1][1]

      Can you tell I don't use multidimensional arrays very often?

      I suppose I was thinking of multidimensional hashes which I think have a different name...
      $hash{'dimension', 'type'};

        $hash{'dimension', 'type'};

        Yes, these exist and are equivalent to

        $hash{ join $;, qw( dimension type ) }
        Not something you should use normally. Probably not even worth remembering, but you already did.

        map{substr$_->[0],$_->[1]||0,1}[\*||{},3],[[]],[ref qr-1,-,-1],[{}],[sub{}^*ARGV,3]

        The multidimensional hashes were actually used for multidimensional array (or hash) emulation, before references were added in perl5 to make those and more complex structures far easier. The old style is still supported, but modern reference-based structures are almost always preferable: see $SUBSCRIPT_SEPARATOR.

        Also, there is nothing wrong with multidimensional arrays. In fact they are so useful, they even have their own manpage: perllol.

Re: Multidimensional arrays
by kcott (Archbishop) on Jun 07, 2023 at 00:06 UTC

    G'day Bod,

    I'm reasonably sure you're not using v5.36; however, the following is probably worth mentioning for anyone else who might be.

    Extract from "perl5360delta: use v5.36":

    The 5.36 bundle also disables the features indirect, and multidimensional. These will forbid, respectively: the use of "indirect" method calls (like $x = new Class;); the use of a list expression as a hash key to simulate sparse multidimensional arrays. The specifics of these changes can be found in feature, but the short version is: this is a bit like having more use strict turned on, disabling features that cause more trouble than they're worth.

    — Ken

      the use of "indirect" method calls (like $x = new Class;)

      Does this apply to filehandles?
      Will we need to write $fh->print("blah"); instead of print $fh "blah";?

      disabling features that cause more trouble than they're worth

      I've not created a multidimensional hash for a long time and don't intend to unless I come across a situation where one makes sense. Such a situation I cannot imagine! But equally, I fail to understand what "trouble" they cause.
      Could you explain please?

      You are right I'm not using v5.36.
      v5.16.3 on the server where this code is running and
      v5.32.1 Strawberry Perl at home/office.

        indirect

        There seems to be a general misunderstanding regarding the equivalence of
        print FILEHANDLE LIST and
        FILEHANDLE->print(LIST).
        They are not equivalent. See ikegami's response to "Re: Optional Subroutine Arguments Like in "print FILEHANDLE LIST"". You may find that a review of print and IO::File is useful here.

        Regarding what v5.36 does (and does not) allow, see the following code.

        #!/usr/bin/env perl use v5.36; use autodie; print "'print'\n"; print STDOUT "'print STDOUT'\n"; print STDERR "'print STDERR'\n"; STDOUT->print("STDOUT->print\n"); STDERR->print("STDERR->print\n"); my $dummy_output = 'test_indirect.out'; { open my $fh, '>', $dummy_output; print $fh "print \$fh\n"; system cat => $dummy_output; } { open my $fh, '>', $dummy_output; $fh->print("\$fh->print\n"); system cat => $dummy_output; } { my %file_handles; open $file_handles{test}, '>', $dummy_output; $file_handles{test}->print("\$file_handles{test}\n"); system cat => $dummy_output; } { my %file_handles; open $file_handles{test}, '>', $dummy_output; print {$file_handles{test}} "print {\$file_handles{test}}\n"; system cat => $dummy_output; } #{ # my %file_handles; # open $file_handles{test}, '>', $dummy_output; # print $file_handles{test} "print \$file_handles{test}\n"; # system cat => $dummy_output; #} use IO::File; { my $fh = IO::File::->new($dummy_output, '>'); print $fh "IO::File: print \$fh\n"; system cat => $dummy_output; } { my $fh = IO::File::->new($dummy_output, '>'); $fh->print("IO::File: \$fh->print\n"); system cat => $dummy_output; } { #use feature 'indirect'; my $fh = new IO::File($dummy_output, '>'); $fh->print("indirect! IO::File: \$fh->print\n"); system cat => $dummy_output; }

        Without Perl 5.36 you won't be able to test this, so let me demonstrate.

        1. Running that as is:
          $ ./test_indirect.pl Bareword found where operator expected at ./test_indirect.pl line 63, +near "new IO::File" (Do you need to predeclare new?) syntax error at ./test_indirect.pl line 63, near "new IO::File" Global symbol "$fh" requires explicit package name (did you forget to +declare "my $fh"?) at ./test_indirect.pl line 64. Execution of ./test_indirect.pl aborted due to compilation errors.
        2. Uncommenting the use feature line:
          $ ./test_indirect.pl 'print' 'print STDOUT' 'print STDERR' STDOUT->print STDERR->print print $fh $fh->print $file_handles{test} print {$file_handles{test}} IO::File: print $fh IO::File: $fh->print indirect! IO::File: $fh->print
          • Discarding stderr:
            $ ./test_indirect.pl 2> /dev/null 'print' 'print STDOUT' STDOUT->print print $fh $fh->print $file_handles{test} print {$file_handles{test}} IO::File: print $fh IO::File: $fh->print indirect! IO::File: $fh->print
          • Discarding stdout:
            $ ./test_indirect.pl 1> /dev/null 'print STDERR' STDERR->print
        3. Uncommenting that anonymous block in the middle of the code:
          $ ./test_indirect.pl String found where operator expected at ./test_indirect.pl line 43, ne +ar "} "print \$file_handles{test}\n"" (Missing operator before "print \$file_handles{test}\n"?) syntax error at ./test_indirect.pl line 43, near "} "print \$file_hand +les{test}\n"" BEGIN not safe after errors--compilation aborted at ./test_indirect.pl + line 47.

        multidimensional

        I had to use that 25-30 years with Perl4. Except for the simplest data structures, I found the syntax to be unwieldy, hard to read, and easy to get wrong. I pretty much jumped with joy when Perl5 came out and I didn't have to use that syntax any more. Admittedly, I had 25-30 years less Perl experience back then — I might find it easier these days (but I'll never know as I have no intention of using it).

        — Ken

Re: Multidimensional arrays
by eyepopslikeamosquito (Archbishop) on Jun 07, 2023 at 01:28 UTC

    G'day Bod,

    Thanks for motivating me to update my old Data Structure References with some modern Perl data structure references.

    As you can see, you are in good company in focusing on your data structures, rather than your code. :)

      Another one of your fascinating links :) - thanks for sharing.

      Great quotes.
      Is The Mythical Man-Month still worth reading, considering it was written in 1975 when I was 6!?

      if you recommend it, I shall attempt to get a copy and add to my (rater long) reading list.

        > Is The Mythical Man-Month still worth reading, considering it was written in 1975 when I was 6!?

        Though first published in 1975, The Mythical Man-Month was republished in an anniversary edition with four extra chapters in 1995, including a reprint of the essay "No Silver Bullet" with commentary by the author.

        Brooks' elegant writing style, unusual in books about computer programming, made it a delight to read for me.

        I'd say it's mandatory reading for managers of large software projects (though not for programmers). The funny thing is that in the 1990s (when I read it) this book had a cult following among my programmer friends ... and yet none of our managers had read it! :)

Re: Multidimensional arrays
by misterperl (Pilgrim) on Jun 06, 2023 at 18:13 UTC
    A birdseye view of what you're up to-

    FIRST what you have looks much better suited to a hash, with the "stripe" as the key. But I'm not sure if that's really sound either because, assuming a product price can go up or down, there's no guarantee of uniqueness. You would know better. But assuming you can implement a unique key, then there are a few other things you need to consider.

    Thinking of your use-case, and that you might want to list prices in chronological order, you probably want a DATE TIME field.

    Another thing to consider is that your SET of products consists of say three durations. What if later you offer a product that is ALSO monthly, but maybe it's an ad-supported version? Well if you use duration as the only product delimiter in the hash key or in the array, now you have a potential issue.

    Finally, your individual hashes look like a SQL table row, with colnames = your hash keys. You'll need to consider how to store and retreive it. NORMALLY, the primary key would be just an auto_increment integer, with N more cols , one for each element in your hashes. Your "stripe" looks like a row identifier perhaps, but you may not need to store that. Normally you would not store compound-elements in your anonymous hash, or the table rows . If you needed keys like those, you can build a hash of them by reading sql query rows into a hash.

    In my head, SPARSE usually means I need a hash, not an array (the MORE sparse a hash is, the faster it can be searched).

      In my head, SPARSE usually means I need a hash, not an array

      That is exactly what prompted the question :)

      Thanks for the extra thought prompts. However, we are not too worried about chronology. It is quite possible that there will be two or more different pricing levels in place simultaneously as different offers are tried out on different audiences. But it's always good to think it through.

Re: Multidimensional arrays
by Arunbear (Prior) on Jun 07, 2023 at 16:57 UTC
    Needing an extra dimension makes me wonder if these new details should also live in the database? One benefit would be to remove much of the duplicated info introduced above (via normalization).
      makes me wonder if these new details should also live in the database

      Yes - they probably should!

      Most of my sites have multiple environments with identical copies of the codebase except for a Variables.pm file that defines things like the DB instance to connect to, Stripe and other keys. Naturally, the Stripe price keys got put in there which is sensible when there are just 3 keys.

      I was planning to extend what I already had. Then I realised that I probably needed a method in the common module for the site to return the correct Stripe price keys. Your observation that these are better stored in the DB really does make sense especially as a method is needed to read them anyway. It may as well just read the database!

      Thanks you pointing out that to which I has cognitive blindness :)

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlquestion [id://11152656]
Approved by choroba
Front-paged by Corion
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others musing on the Monastery: (3)
As of 2024-04-26 07:12 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found