Function
You could fix your function (errors marked with !!! ):
CREATE OR REPLACE FUNCTION pg_temp.org(volunteer_id VARCHAR)
RETURNS CHAR AS $$
DECLARE
dob_individualized VARCHAR(9);
dob VARCHAR(9); -- VARCHAR(6) was too short !!!
mid_character CHAR(1);
control_character CHAR(1);
remainder INT;
BEGIN
-- Extract date of birth and individualized string from the volunteer ID
dob := SUBSTRING(volunteer_id FROM 1 FOR 6);
dob_individualized := SUBSTRING(volunteer_id FROM 8 FOR 3); -- offset 7 was wrong !!!
-- Determine the mid-character based on the year of birth
CASE
WHEN dob BETWEEN '000001' AND '999999' THEN mid_character := '+';
WHEN dob BETWEEN '00A001' AND '99F999' THEN mid_character := '-';
ELSE mid_character := '';
END CASE;
-- Concatenate the date of birth and individualized string
dob := dob || dob_individualized;
-- Calculate the remainder
remainder := CAST(dob AS INT) % 31;
-- Determine the control character
CASE remainder
WHEN 10 THEN control_character := 'A';
WHEN 11 THEN control_character := 'B';
WHEN 12 THEN control_character := 'C';
WHEN 13 THEN control_character := 'D';
WHEN 14 THEN control_character := 'E';
WHEN 15 THEN control_character := 'F';
WHEN 16 THEN control_character := 'H';
WHEN 17 THEN control_character := 'J';
WHEN 18 THEN control_character := 'K';
WHEN 19 THEN control_character := 'L';
WHEN 20 THEN control_character := 'M';
WHEN 21 THEN control_character := 'N';
WHEN 22 THEN control_character := 'P';
WHEN 23 THEN control_character := 'R';
WHEN 24 THEN control_character := 'S';
WHEN 25 THEN control_character := 'T';
WHEN 26 THEN control_character := 'U';
WHEN 27 THEN control_character := 'V';
WHEN 28 THEN control_character := 'W';
WHEN 29 THEN control_character := 'X';
WHEN 30 THEN control_character := 'Y';
ELSE control_character := CAST(remainder AS CHAR(1));
END CASE;
RETURN control_character;
END;
$$ LANGUAGE plpgsql;
But most of it is just noise. Boils down to:
CREATE OR REPLACE FUNCTION calculate_control_character(volunteer_id text)
RETURNS text
LANGUAGE sql IMMUTABLE PARALLEL SAFE STRICT
RETURN ('[0:30]={0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F,H,J,K,L,M,N,P,R,S,T,U,V,W,X,Y}'::text[])
[(left($1, 6) || substring($1, 8, 3))::int % 31];
Basically picks an array element according to the position computed with your formula.
Using the short syntax of standard SQL functions. See:
Add appropriate function labels. In this case: IMMUTABLE, PARALLEL SAFE, STRICT. The first two are crucial for performance! See:
Postgres arrays are 1-based by default. The modulo operator % returns 0-based. You can either add 1, or work with a custom-index array (cheaper). See:
Don't use the data type char(n). See:
CHECK constraint
Also a bit cheaper and shorter:
ALTER TABLE volunteer
ADD CONSTRAINT chk_validvolunteerid CHECK (
length(id) = 11
AND substring(id, 7, 1) = ANY ('{+,-,A,B,C,D,E,F,X,Y,W,V,U}')
AND right(id, 1) = calculate_control_character(id)
);
You could even just use the expression from my condensed function directly without creating a function at all.
Just remember to document what you are doing and why, either way.