Thread: Novice PL/pgSQL question and example

Novice PL/pgSQL question and example

From
James Long
Date:
At least, I hope this is a Novice question.  It sure makes me
feel like one! :)  Please cc: me directly on replies, as I might
miss a reply that goes only to the list.

I'm running PostGreSQL 8.3.9 on FreeBSD 6.4-STABLE, but I doubt
this is platform-dependent.

I'm trying to solve what I'm sure is a common problem in the
accounting world.  A total cost C has to be split across N
different accounts, and C is not necessarily divisible by N.
Shares with fractions of pennies are not allowable, and to make
the accounting balance, the sum of all the shares has to sum to
exactly match the total cost C.

It's like splitting a $90 lunch check between 7 people.

This PHP code shows the math I'm using, and works great.  For
clarity to those not familiar with PHP, the &$error in the
calc_share function declaration means that parameter $error is
passed by reference, not by value.  This is necessary because the
calc_share function has to keep track of the cumulative error
from one share to the next that arises from taking a real number
and rounding it to two decimal places.  Eventually, the
cumulative error will be sufficient to bump a share up or down in
value by one penny.  I'm using OUT parameters in the PL/pgSQL
version to achieve this result.


<?php
function calc_share( $amount, $shares, &$error )
{
        $share = $amount / $shares;
        $result = round( $share + $error, 2 );
        $error += $share - $result;
        return $result;
}


$amount = 90;
$shares = 7;

$error_term = 0;
$test_sum = 0;

for ( $i = 0; $i < $shares; ) {
  ++$i;
  $one_share = calc_share( $amount, $shares, $error_term );
  print "i = " . $i . " " . $one_share . "\n";
  $test_sum = $test_sum + $one_share;
}
print "sum = " . $test_sum . "\n";
?>

Here is my attempt to implement this in PL/PGSQL.

The commented-out UPDATE query is how I would like to use this
function in a real-world application (given that there are exactly 7
rows in my table WHERE reference = 'SHARE').

But for now, I'm using the FOR loop and the RAISE NOTICE just to
try to see what's going on, and whether my algorithm is working
correctly.

It's not.


--- begin test.sql
CREATE OR REPLACE FUNCTION calc_share( cost REAL, N_shares INTEGER,
                OUT error_term REAL, OUT result REAL ) AS $$

-- When called N consecutive times with identical values of
-- COST and N_SHARES, this routine will calculate N shares of a
-- value COST and keep track of the error term, so that some shares
-- may be one penny higher than other shares, but the sum of all the
-- shares will always match the total COST.

-- The caller must set error_term to 0 before the first call.

DECLARE
  one_share REAL;
  result REAL;

BEGIN
  one_share := cost / N_shares;
  result := ROUND( one_share + error_term, 2 );
  error_term := error_term + one_share - result;
END;

$$ LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION share_costs() RETURNS VOID AS $$

DECLARE
  error_term    REAL;
  shares        INTEGER;
  i             INTEGER;
  cost          REAL;
  one_share     REAL;

BEGIN
  error_term := 0;
  cost := 90;
  shares := 7;

--  update my_table set amount = calc_share( cost, shares, error_term ) where reference = 'SHARE';

  error_term := 0;
  FOR i IN 1 .. shares LOOP
    PERFORM calc_share( cost, shares, error_term, one_share );
    RAISE NOTICE 'i = %, share = %', i, one_share;
  END LOOP;
END;

$$ LANGUAGE PLPGSQL;
--- end test.sql


I'm trying to invoke this code thusly:

$ psql
Welcome to psql 8.3.9, the PostgreSQL interactive terminal.

Type:  \copyright for distribution terms
       \h for help with SQL commands
       \? for help with psql commands
       \g or terminate with semicolon to execute query
       \q to quit

pnwc=> \i test.sql
CREATE FUNCTION
CREATE FUNCTION
pnwc=> \df
(trimmed)
 public     | calc_share                                   | record                      | cost real, n_shares integer,
OUTerror_term real, OUT result real 
 public     | share_costs                                  | void                        |
(1857 rows)

pnwc=> select share_costs();


Here I would expect to see 7 rows of output showing the 7
consecutive share values that were calculated.  Instead:

ERROR:  function calc_share(real, integer, real, real) does not exist
LINE 1: SELECT  calc_share(  $1 ,  $2 ,  $3 ,  $4  )
                ^
HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
QUERY:  SELECT  calc_share(  $1 ,  $2 ,  $3 ,  $4  )
CONTEXT:  PL/pgSQL function "share_costs" line 15 at PERFORM


My novice eyes don't see that the function doesn't exist, or that
the types of the parameters are not matched correctly to the
function declaration.  What am I missing that is preventing this
function from working as I would like it to?

Thank you.  I appreciate your time.


Regards,

Jim


Re: Novice PL/pgSQL question and example

From
James Long
Date:
Refinement:

I see from a closer reading of the docs that OUT parameters cannot
be passed to functions -- the error message was trying to tell me
that I had a mismatch in the NUMBER of parameters, not necessarily
their types.  However, "ROUND()" forced me to make some type
changes and casts as necessary.

So the "calc_share" function is now declared as:

CREATE OR REPLACE FUNCTION calc_share( cost NUMERIC, N_shares INTEGER,
                INOUT error_term REAL, OUT result NUMERIC ) AS $$

-- When called N consecutive times with identical values of
-- COST and N_SHARES, this routine will calculate N shares of a
-- value COST and keep track of the error term, so that some shares
-- may be one penny higher than other shares, but the sum of all the
-- shares will always match the total COST.

-- The caller must set error_term to 0 before the first call.

DECLARE
    one_share   REAL;
    result      NUMERIC;

BEGIN
    one_share := cost / N_shares;
    result := ROUND( CAST( one_share + error_term AS NUMERIC), 2 );
    error_term := error_term + one_share - result;
END;



and I can get the routine to at least execute by using:

...
  FOR i IN 1 .. shares LOOP
    SELECT * FROM calc_share( cost, shares, error_term ) into error_term, one_share;
    RAISE NOTICE 'i = %, share = %', i, one_share;
  END LOOP;
...

But that SELECT syntax doesn't return any value into "one_share".

The output I get now is:

pnwc=> \i test.sql
CREATE FUNCTION
CREATE FUNCTION
pnwc=> select share_costs();
NOTICE:  i = 1, share = <NULL>
NOTICE:  i = 2, share = <NULL>
NOTICE:  i = 3, share = <NULL>
NOTICE:  i = 4, share = <NULL>
NOTICE:  i = 5, share = <NULL>
NOTICE:  i = 6, share = <NULL>
NOTICE:  i = 7, share = <NULL>
 share_costs
-------------

(1 row)



On Sun, Feb 07, 2010 at 05:33:29PM -0800, James Long wrote:
> At least, I hope this is a Novice question.  It sure makes me
> feel like one! :)  Please cc: me directly on replies, as I might
> miss a reply that goes only to the list.
>
> I'm running PostGreSQL 8.3.9 on FreeBSD 6.4-STABLE, but I doubt
> this is platform-dependent.
>
> I'm trying to solve what I'm sure is a common problem in the
> accounting world.  A total cost C has to be split across N
> different accounts, and C is not necessarily divisible by N.
> Shares with fractions of pennies are not allowable, and to make
> the accounting balance, the sum of all the shares has to sum to
> exactly match the total cost C.
>
> It's like splitting a $90 lunch check between 7 people.
>
> This PHP code shows the math I'm using, and works great.  For
> clarity to those not familiar with PHP, the &$error in the
> calc_share function declaration means that parameter $error is
> passed by reference, not by value.  This is necessary because the
> calc_share function has to keep track of the cumulative error
> from one share to the next that arises from taking a real number
> and rounding it to two decimal places.  Eventually, the
> cumulative error will be sufficient to bump a share up or down in
> value by one penny.  I'm using OUT parameters in the PL/pgSQL
> version to achieve this result.
>
>
> <?php
> function calc_share( $amount, $shares, &$error )
> {
>         $share = $amount / $shares;
>         $result = round( $share + $error, 2 );
>         $error += $share - $result;
>         return $result;
> }
>
>
> $amount = 90;
> $shares = 7;
>
> $error_term = 0;
> $test_sum = 0;
>
> for ( $i = 0; $i < $shares; ) {
>   ++$i;
>   $one_share = calc_share( $amount, $shares, $error_term );
>   print "i = " . $i . " " . $one_share . "\n";
>   $test_sum = $test_sum + $one_share;
> }
> print "sum = " . $test_sum . "\n";
> ?>
>
> Here is my attempt to implement this in PL/PGSQL.
>
> The commented-out UPDATE query is how I would like to use this
> function in a real-world application (given that there are exactly 7
> rows in my table WHERE reference = 'SHARE').
>
> But for now, I'm using the FOR loop and the RAISE NOTICE just to
> try to see what's going on, and whether my algorithm is working
> correctly.
>
> It's not.
>
>
> --- begin test.sql
> CREATE OR REPLACE FUNCTION calc_share( cost REAL, N_shares INTEGER,
>                 OUT error_term REAL, OUT result REAL ) AS $$
>
> -- When called N consecutive times with identical values of
> -- COST and N_SHARES, this routine will calculate N shares of a
> -- value COST and keep track of the error term, so that some shares
> -- may be one penny higher than other shares, but the sum of all the
> -- shares will always match the total COST.
>
> -- The caller must set error_term to 0 before the first call.
>
> DECLARE
>   one_share REAL;
>   result REAL;
>
> BEGIN
>   one_share := cost / N_shares;
>   result := ROUND( one_share + error_term, 2 );
>   error_term := error_term + one_share - result;
> END;
>
> $$ LANGUAGE plpgsql;
>
> CREATE OR REPLACE FUNCTION share_costs() RETURNS VOID AS $$
>
> DECLARE
>   error_term    REAL;
>   shares        INTEGER;
>   i             INTEGER;
>   cost          REAL;
>   one_share     REAL;
>
> BEGIN
>   error_term := 0;
>   cost := 90;
>   shares := 7;
>
> --  update my_table set amount = calc_share( cost, shares, error_term ) where reference = 'SHARE';
>
>   error_term := 0;
>   FOR i IN 1 .. shares LOOP
>     PERFORM calc_share( cost, shares, error_term, one_share );
>     RAISE NOTICE 'i = %, share = %', i, one_share;
>   END LOOP;
> END;
>
> $$ LANGUAGE PLPGSQL;
> --- end test.sql
>
>
> I'm trying to invoke this code thusly:
>
> $ psql
> Welcome to psql 8.3.9, the PostgreSQL interactive terminal.
>
> Type:  \copyright for distribution terms
>        \h for help with SQL commands
>        \? for help with psql commands
>        \g or terminate with semicolon to execute query
>        \q to quit
>
> pnwc=> \i test.sql
> CREATE FUNCTION
> CREATE FUNCTION
> pnwc=> \df
> (trimmed)
>  public     | calc_share                                   | record                      | cost real, n_shares
integer,OUT error_term real, OUT result real 
>  public     | share_costs                                  | void                        |
> (1857 rows)
>
> pnwc=> select share_costs();
>
>
> Here I would expect to see 7 rows of output showing the 7
> consecutive share values that were calculated.  Instead:
>
> ERROR:  function calc_share(real, integer, real, real) does not exist
> LINE 1: SELECT  calc_share(  $1 ,  $2 ,  $3 ,  $4  )
>                 ^
> HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
> QUERY:  SELECT  calc_share(  $1 ,  $2 ,  $3 ,  $4  )
> CONTEXT:  PL/pgSQL function "share_costs" line 15 at PERFORM
>
>
> My novice eyes don't see that the function doesn't exist, or that
> the types of the parameters are not matched correctly to the
> function declaration.  What am I missing that is preventing this
> function from working as I would like it to?
>
> Thank you.  I appreciate your time.
>
>
> Regards,
>
> Jim
>
>
> --
> Sent via pgsql-novice mailing list (pgsql-novice@postgresql.org)
> To make changes to your subscription:
> http://www.postgresql.org/mailpref/pgsql-novice

Re: Novice PL/pgSQL question and example

From
Tom Lane
Date:
James Long <pgsql-novice@museum.rain.com> writes:
> So the "calc_share" function is now declared as:

> CREATE OR REPLACE FUNCTION calc_share( cost NUMERIC, N_shares INTEGER,
>                 INOUT error_term REAL, OUT result NUMERIC ) AS $$

> -- When called N consecutive times with identical values of
> -- COST and N_SHARES, this routine will calculate N shares of a
> -- value COST and keep track of the error term, so that some shares
> -- may be one penny higher than other shares, but the sum of all the
> -- shares will always match the total COST.

> -- The caller must set error_term to 0 before the first call.

> DECLARE
>     one_share   REAL;
>     result      NUMERIC;

> BEGIN
>     one_share := cost / N_shares;
>     result := ROUND( CAST( one_share + error_term AS NUMERIC), 2 );
>     error_term := error_term + one_share - result;
> END;

Hi James,

I think your problem is that you've got a local variable "result"
masking the OUT parameter.  The assignment in the function body
assigns to that local variable, not to the OUT parameter.

BTW, you might also have some issues around having multiple versions
of calc_share() with different parameter lists --- you mentioned
having "cost" declared as both numeric and real.  You might be seeing
the thing call a different version than you were expecting.

            regards, tom lane

Re: Novice PL/pgSQL question and example

From
James Long
Date:
On Mon, Feb 08, 2010 at 01:51:16AM -0500, Tom Lane wrote:
> James Long <pgsql-novice@museum.rain.com> writes:
> > So the "calc_share" function is now declared as:

Actually, error_term is now NUMERIC also:

> > CREATE OR REPLACE FUNCTION calc_share( cost NUMERIC, N_shares INTEGER,
> >                 INOUT error_term REAL, OUT result NUMERIC ) AS $$
>
> > -- When called N consecutive times with identical values of
> > -- COST and N_SHARES, this routine will calculate N shares of a
> > -- value COST and keep track of the error term, so that some shares
> > -- may be one penny higher than other shares, but the sum of all the
> > -- shares will always match the total COST.
>
> > -- The caller must set error_term to 0 before the first call.
>
> > DECLARE
> >     one_share   REAL;
> >     result      NUMERIC;
>
> > BEGIN
> >     one_share := cost / N_shares;
> >     result := ROUND( CAST( one_share + error_term AS NUMERIC), 2 );
> >     error_term := error_term + one_share - result;
> > END;
>
> Hi James,
>
> I think your problem is that you've got a local variable "result"
> masking the OUT parameter.  The assignment in the function body
> assigns to that local variable, not to the OUT parameter.
>
> BTW, you might also have some issues around having multiple versions
> of calc_share() with different parameter lists --- you mentioned
> having "cost" declared as both numeric and real.  You might be seeing
> the thing call a different version than you were expecting.
>
>             regards, tom lane

Gee, that duplication of the 'result' parameter should have been obvious.
Thank you for pointing it out.  And yes, I did a \df and saw that I did
have a number of overloaded calc_share() functions with various parameter
types.  After removing the local declaration of 'result' and cleaning out
the extraneous calc_share() definitions, it now works in debug mode:

pnwc=> \i test.sql
CREATE FUNCTION
CREATE FUNCTION
pnwc=> select share_costs();
NOTICE:  i = 1, share = 12.86
NOTICE:  i = 2, share = 12.85
NOTICE:  i = 3, share = 12.86
NOTICE:  i = 4, share = 12.86
NOTICE:  i = 5, share = 12.86
NOTICE:  i = 6, share = 12.85
NOTICE:  i = 7, share = 12.86
 share_costs
-------------

(1 row)


Thank you very much!

Jim


Re: Novice PL/pgSQL question and example

From
James Long
Date:
Pardon the edit, but I'll delete the previous quotations, since
the issues of syntax and functionality are resolved.  Thank you, Tom.

The next problem is simplicity, or elegance, if you like.

I would like the code to read like this:

CREATE OR REPLACE FUNCTION share_cost( cost NUMERIC ) RETURNS VOID AS $$

-- Spread the amount COST across a number of records WHERE reference = 'SHARE'
-- increment the AMOUNT field by the amount of a nearly-equal share, so that
-- the sum of the shares exactly equals COST.

DECLARE
  shares        INTEGER;
  error_term    NUMERIC;

BEGIN

  SELECT SUM(1) from temp WHERE reference = 'SHARE' into shares;

  error_term := 0;
  UPDATE temp SET amount = amount + calc_share( cost, shares, error_term )
    WHERE reference = 'SHARE';

END;

$$ LANGUAGE PLPGSQL;


This example has the advantage of not requiring a primary key on
my temporary table, since the UPDATE statement ensures that each
record is processed in turn, with no ambiguity as to which record
is being updated.

However, the "calc_share" function has one INOUT parameter
"error_term" and an OUT parameter "result".  From what I gather
so far, PL/pgSQL does not allow a function with OUT or INOUT
parameters to return a scalar result value.  Based on that
understanding, my code looks like:

CREATE OR REPLACE FUNCTION share_cost( cost NUMERIC ) RETURNS VOID AS $$

-- Spread the amount COST across a number of records WHERE reference = 'SHARE'
-- increment the AMOUNT field by the amount of a nearly-equal share, so that
-- the sum of the shares exactly equals COST.

DECLARE
  shares        INTEGER;
  error_term    NUMERIC;
  one_share     NUMERIC;
  share_record  RECORD;

BEGIN

  SELECT SUM(1) from temp WHERE reference = 'SHARE' into shares;

  error_term := 0;
  FOR share_record IN SELECT * FROM temp WHERE reference = 'SHARE' LOOP
    SELECT error_term, result FROM calc_share( cost, shares, error_term ) INTO error_term, one_share;
    UPDATE temp SET amount = amount + one_share
        WHERE temp.acct_id = share_record.acct_id;
  END LOOP;

END;

$$ LANGUAGE PLPGSQL;

So the simple UPDATE statement in the first example becomes a
somewhat clunky FOR loop in the second example, and the second
example also requires a primary key on acct_id to ensure that the
UPDATE and the FOR loop reference the same record.

Is that as good as this can get, or is there a simpler way, more
along the lines of the first version?

Thanks again for the education.

Jim


Re: Novice PL/pgSQL question and example

From
Tim Landscheidt
Date:
James Long <pgsql-novice@museum.rain.com> wrote:

> [...]
> Is that as good as this can get, or is there a simpler way, more
> along the lines of the first version?

I'm not certain that I get the gist of your share_costs ()
function, but why not just something functional along the
lines of:

| tim=# SELECT G.A, TRUNC(90.0 / 7, 2) +
| tim-#             CASE
| tim-#               WHEN ROW_NUMBER() OVER (ORDER BY G.A) <= 100 * (90 - 7 * TRUNC(90.0 / 7, 2)) THEN
| tim-#                 0.01
| tim-#               ELSE
| tim-#                 0.00
| tim-#               END
| tim-#   FROM generate_series(1, 7) AS G(A);
|  a | ?column?
| ---+----------
|  1 |    12.86
|  2 |    12.86
|  3 |    12.86
|  4 |    12.86
|  5 |    12.86
|  6 |    12.85
|  7 |    12.85
| (7 Zeilen)

| tim=#

Tim

Re: Novice PL/pgSQL question and example

From
James Long
Date:
An interesting approach, one of which I wouldn't have conceived.
I am trying to understand how it works.  You calculate a first
approximation based on discarding the fractional penny, and
then adjust that by adding on a whole penny in some cases, so
that the sum of the shares matches the original amount that
was divided.

Actually, on my 8.3.9, it doesn't work:

pnwc=> SELECT G.A, TRUNC(90.0 / 7, 2) +
pnwc->              CASE
pnwc->                WHEN ROW_NUMBER() OVER (ORDER BY G.A) <= 100 * (90 - 7 * TRUNC(90.0 / 7, 2)) THEN
pnwc->                  0.01
pnwc->                ELSE
pnwc->                  0.00
pnwc->                END
pnwc->    FROM generate_series(1, 7) AS G(A);
ERROR:  syntax error at or near "OVER"
LINE 3:                WHEN ROW_NUMBER() OVER (ORDER BY G.A) <= 100 ...
                                         ^

What version are you running, or what am I doing wrong that
prevents me from reproducing your results?

Thanks!

Jim


On Mon, Feb 08, 2010 at 09:17:38PM +0000, Tim Landscheidt wrote:
> James Long <pgsql-novice@museum.rain.com> wrote:
>
> > [...]
> > Is that as good as this can get, or is there a simpler way, more
> > along the lines of the first version?
>
> I'm not certain that I get the gist of your share_costs ()
> function, but why not just something functional along the
> lines of:
>
> | tim=# SELECT G.A, TRUNC(90.0 / 7, 2) +
> | tim-#             CASE
> | tim-#               WHEN ROW_NUMBER() OVER (ORDER BY G.A) <= 100 * (90 - 7 * TRUNC(90.0 / 7, 2)) THEN
> | tim-#                 0.01
> | tim-#               ELSE
> | tim-#                 0.00
> | tim-#               END
> | tim-#   FROM generate_series(1, 7) AS G(A);
> |  a | ?column?
> | ---+----------
> |  1 |    12.86
> |  2 |    12.86
> |  3 |    12.86
> |  4 |    12.86
> |  5 |    12.86
> |  6 |    12.85
> |  7 |    12.85
> | (7 Zeilen)
>
> | tim=#
>
> Tim
>
> --
> Sent via pgsql-novice mailing list (pgsql-novice@postgresql.org)
> To make changes to your subscription:
> http://www.postgresql.org/mailpref/pgsql-novice

Re: Novice PL/pgSQL question and example

From
Tim Landscheidt
Date:
James Long <pgsql-novice@museum.rain.com> wrote:

> An interesting approach, one of which I wouldn't have conceived.
> I am trying to understand how it works.  You calculate a first
> approximation based on discarding the fractional penny, and
> then adjust that by adding on a whole penny in some cases, so
> that the sum of the shares matches the original amount that
> was divided.

I wouldn't use that terminology if I would present it to
someone in accounting, but essentially: Yes.

> Actually, on my 8.3.9, it doesn't work:

> pnwc=> SELECT G.A, TRUNC(90.0 / 7, 2) +
> pnwc->              CASE
> pnwc->                WHEN ROW_NUMBER() OVER (ORDER BY G.A) <= 100 * (90 - 7 * TRUNC(90.0 / 7, 2)) THEN
> pnwc->                  0.01
> pnwc->                ELSE
> pnwc->                  0.00
> pnwc->                END
> pnwc->    FROM generate_series(1, 7) AS G(A);
> ERROR:  syntax error at or near "OVER"
> LINE 3:                WHEN ROW_NUMBER() OVER (ORDER BY G.A) <= 100 ...
>                                          ^

> What version are you running, or what am I doing wrong that
> prevents me from reproducing your results?
> [...]

The ROW_NUMBER() construct is a windowing function intro-
duced in 8.4. You can either use one of the workarounds
listed in
<URI:http://www.postgresonline.com/journal/index.php?/archives/79-Simulating-Row-Number-in-PostgreSQL-Pre-8.4.html>
or, if you prefer an imperative approach, loop over the rows
to update and increment your own counter.

Tim

Re: Novice PL/pgSQL question and example

From
Jasen Betts
Date:
On 2010-02-08, James Long <pgsql-novice@museum.rain.com> wrote:

Newsgroups: gmane.comp.db.postgresql.novice
From: Jasen Betts <jasen@xnet.co.nz>
Subject: Re: Novice PL/pgSQL question and example
References: <20100208013329.GA95096@ns.umpquanet.com>
Organization: Dis (not Dat) Organisation
Followup-To:
X-Face:
?)Aw4rXwN5u0~$nqKj`xPz>xHCwgi^q+^?Ri*+R(&uv2=E1Q0Zk(>h!~o2ID@6{uf8s;a+M[5[U[QT7xFN%^gR"=tuJw%TXXR'Fp~W;(T"1(739R%m0Yyyv*gkGoPA.$b,D.w:z+<'"=-lVT?6{T?=R^:W5g|E2#EhjKCa+nt":4b}dU7GYB*HBxn&Td$@f%.kl^:7X8rQWd[NTc"P"u6nkisze/Q;8"9Z{peQF,w)7UjV$c|RO/mQW/NMgWfr5*$-Z%u46"/00mx-,\R'fLPe.)^

On 2010-02-08, James Long <pgsql-novice@museum.rain.com> wrote:
> At least, I hope this is a Novice question.  It sure makes me
> feel like one! :)  Please cc: me directly on replies, as I might
> miss a reply that goes only to the list.
>
> I'm running PostGreSQL 8.3.9 on FreeBSD 6.4-STABLE, but I doubt
> this is platform-dependent.
>
> I'm trying to solve what I'm sure is a common problem in the
> accounting world.  A total cost C has to be split across N
> different accounts, and C is not necessarily divisible by N.
> Shares with fractions of pennies are not allowable, and to make
> the accounting balance, the sum of all the shares has to sum to
> exactly match the total cost C.
>
> It's like splitting a $90 lunch check between 7 people.
>

> This PHP code shows the math I'm using, and works great.

here's different way to do it.

 share ( pieces , people, index ) = floor (pieces * (index + 1) / people ) - floor ( pieces * index / people)

you don't need to remember an error just increase the index each time.