Mysql function to genarate custom ids - mysql

I need some help in creating a MySQL function
This function generates a user id for my user, Which generates 5 digits unique id starting from A0001, A0002, B0001, C0001, and so on but the problem is it reaches F9999 as per my function the following number should be G0000
But my requirement is can't go past letter F
We can't have a user id that is more than 5 'digits' and we can only use the letters A to F
Se I come with some Solution moving on to a range that is something like this: AA000, AA001, AA002.... and then AB000, AB001, AB002, AF999 BA000, etc.
This is my current function which I use to generate userid
DELIMITER $$
CREATE DEFINER=`root`#`localhost` FUNCTION `getNextID`() RETURNS varchar(10) CHARSET utf8
BEGIN
set #prefix := (select COALESCE(max(left(id, 1)), 'A') from users where left(id, 1) < 1);
set #highest := (select max(CAST(right(id, 4) AS UNSIGNED))+1 from users where left(id, 1) = #prefix);
if #highest > 9999 then
set #prefix := CHAR(ORD(#prefix)+1);
set #highest := 0;
end if;
RETURN concat( #prefix , LPAD( #highest, 4, 0 ) );
END$$
DELIMITER ;

Your ID can be thought of a hexadecimal number consisting of letters only, followed by a decimal number. Each hexadecimal digit starts a new series of decimal numbers, because the ID is of fixed length 5.
The first subproblem is to find the maximum ID, because it should be assumed that F9998 < F9999 < AA000 < AA001. We can calculate H*10000 + D with H being the hexadecimal part and D the decimal part of the ID to get the right order.
SELECT id
FROM (
SELECT 'AB999' as id UNION
SELECT 'AA000' UNION
SELECT 'F9999' UNION
SELECT 'AAA00' UNION
SELECT 'FFFF9' UNION
SELECT 'FFFF8' UNION
SELECT 'FFFD3') user
ORDER BY conv(regexp_substr(id, '^[A-F]*'), 16, 10) * 10000 + CAST(substring(id, length(regexp_substr(id, '^[A-F]*')) + 1) AS unsigned) DESC
LIMIT 1;
The second subproblem is to find the successor of a given ID. We calculate the decimal number like above but use the correct factor (10^n with n being the length of the decimal part) this time, then we add one to this number and convert it back to the hex/dec representation. In the hexadecimal part there may be 0s and 1s which have to be replaced by 'A'. Whenever the hex part gets longer, the decimal part consists of 0s only. That is, we can just return a substring of the desired length and strip trailing 0es:
DELIMITER //
CREATE FUNCTION nextId(id VARCHAR(5)) RETURNS VARCHAR(5) NO SQL
BEGIN
set #hexStr := regexp_substr(id, '^[A-F]*');
set #digits := length(id) - length(#hexStr);
set #decimalPart := CAST(right(id, #digits) AS UNSIGNED);
set #factor := pow(10, #digits);
set #hexPart := conv(#hexStr, 16, 10);
set #n := #hexPart * #factor + #decimalPart + 1; -- ID increased by 1
set #decimalPart := mod(#n, #factor);
set #hexStr := regexp_replace(conv(floor(#n / #factor), 10, 16), '[01]', 'A');
return substring(concat(#hexStr, lpad(#decimalPart, #digits, '0')), 1, length(id));
END;
//
DELIMITER ;
Using this function
SELECT id, nextId(id) next_id
FROM (
SELECT 'F9998' as id UNION
SELECT 'F9999' UNION
SELECT 'AA999' as id UNION
SELECT 'AB000' UNION
SELECT 'AB999' UNION
SELECT 'AF999' UNION
SELECT 'FF999' UNION
SELECT 'AAA00') user;
results in
id
next_id
F9998
F9999
F9999
AA000
AA999
AB000
AB000
AB001
AB999
AC000
AF999
BA000
FF999
AAA00
AAA00
AAA01
Here's a fiddle

Related

Teradata SQL Split Single String into Table Rows

I have one string element, for example : "(1111, Tem1), (0000, Tem2)" and hope to generate a data table such as
var1
var2
1111
Tem1
0000
Tem2
This is my code, I created the lag token and filter with odd rows element.
with var_ as (
select '(1111, Tem1), (0000, Tem2)' as pattern_
)
select tbb1.*, tbb2.result_string as result_string_previous
from(
select tb1.*,
min(token) over(partition by 1 order by token asc rows between 1 preceding and 1 preceding) as min_token
from
table (
strtok_split_to_table(1, var_.pattern_, '(), ')
returns (outkey INTEGER, token INTEGER, result_string varchar(20))
) as tb1) tbb1
inner join (select min_token, result_string from tbb1) tbb2
on tbb1.token = tbb2.min_token
where (token mod 2) = 0;
But it seems that i can't generate new variables in "from" step and applied it directly in "join" step.
so I wanna ask is still possible to get the result what i want in my procedure? or is there any suggestion?
Thanks for all your assistance.
I wouldn't split / recombine the groups. Split each group to a row, then split the values within the row, e.g.
with var_ as (
select '(1111, Tem1), (0000, Tem2)' as pattern_
),
split1 as (
select trim(leading '(' from result_string) as string_
from
table ( /* split at & remove right parenthesis */
regexp_split_to_table(1, var_.pattern_, '\)((, )|$)','c')
returns (outkey INTEGER, token_nbr INTEGER, result_string varchar(256))
) as tb1
)
select *
from table(
csvld(split1.string_, ',', '"')
returns (var1 VARCHAR(16), var2 VARCHAR(16))
) as tb2
;

MySQL procedure to check next free value

How should i write procedure which will check next free number (which is string) based on input.
Procedure should have 2 input value, first one is user input (numbers) and second one is maximum amount of characters in string.
This is procedure i tried to write:
CREATE DEFINER=`root`#`localhost` PROCEDURE `getfreenum`(IN num CHAR(20), IN maxval CHAR(20))
begin
set #compare := (num + num *10);
set #maxId := (select sifra from artikli where sifra >= #compare order by sifra asc limit 1);
while #compare = #maxId do
set #compare := #compare + 1;
set #maxId = (select sifra from artikli where sifra >= #compare order by sifra asc limit 1);
end while;
select #compare;
end
This procedure finds me next available value after my input but it does not include my input in that number, meaning if i call procedure Call getfreenum(1,5) i get value 779 but i should get next 5 char value which includes input number, which is 1.
So a Call getfreenum(1,5) procedure should return 10043 if everything is taken from 10000 to and including 100042 and Call getfreenum(11,5) should check for remaining 3 characters and return let's say 11000. Or 11600 if everything is taken from 11000 to 11599. And it should work like that for every input, even if i enter 4 characters: Call getfreenum(1234,5) procedure should check for 12340, 12341, 12342 and if 12349 is free it should return that, but it should not return value which changes input number, meaning, if I call Call getfreenum(1234,5) and everything is taken including '123459' then function should return NULL or some fixed value for all errors.
Function is used for asigning item numbers to items in a shop. Sometimes for some items maximum number of digits is 3 and sometimes it is 5. Some items have starting numbers: let's say "1254" for cigarettes. and "12" are starting numbers for luxury goods. It is easier for cashier to use this logic when assigning item numbers. it's just more complex for me :) #Schwern – stacks 3 mins ago
This is better handled with better schema design.
Recognize that "1254" is really two parts. There's the category ID 12 and the item ID 54. Rather than store "1254" you'd store these two separately.
create table item_categories(
id integer primary key auto_increment,
shop_id_padding integer not null default 5,
name text not null
);
create table items (
id integer primary key auto_increment,
name text not null,
category_id integer not null,
foreign key(category_id) references item_categories(id)
);
An explicit item_categories table gives us referential integrity, a place to store the category name, as well as how much padding to use.
Now you can let auto_increment do its job.
insert into item_categories (id, name, shop_id_padding) values (12, "cigarettes", 2);
insert into items (name, category_id) values ("Coffin Nails", 12);
insert into items (name, category_id) values ("Death Sticks", 12);
select * from items;
+----+--------------+-------------+
| id | name | category_id |
+----+--------------+-------------+
| 1 | Coffin Nails | 12 |
| 2 | Death Sticks | 12 |
+----+--------------+-------------+
Construct the shop ID using concat. Pad the ID according to the category.
select concat(i.category_id, lpad(i.id, cat.shop_id_padding, '0')) as shop_id
from items i
join item_categories cat on i.category_id = cat.id;
+---------+
| shop_id |
+---------+
| 1201 |
| 1202 |
+---------+
You can get the set of all sifra where sifra + 1 does not exist with NOT EXISTS and a correlated subquery.
num * power(10, maxval - floor(log10(num)) - 1) gives you the minimum number, e.g. 21000 for 21, 5 and num * power(10, maxval - floor(log10(num)) - 1) + power(10, maxval - floor(log10(num)) - 1) gives you one more than the maximum number, e.g. 22000 for 21, 5. Compare sifra + 1 against it accordingly.
Finally you have to make sure the requested number of digits does not exceed the number of digits for the given prefix. I.e. floor(log10(num)) < maxval has to be satisfied.
To make sure at least the predecessor num * power(10, maxval - floor(log10(num)) - 1) - 1 of the minimum number exists use UNION ALL to add it to the base set.
Like that you can calculate the number with just a SELECT, without any (possibly relatively slow) loops.
CREATE PROCEDURE getfreenum
(IN num integer,
IN maxval integer)
BEGIN
SELECT CASE
WHEN NOT EXISTS (SELECT *
FROM artikli t2
WHERE t2.sifra = num * power(10, maxval - floor(log10(num)) - 1)) THEN
num * power(10, maxval - floor(log10(num)) - 1)
ELSE
min(t1.sifra) + 1
END sifra
FROM artikli t1
WHERE floor(log10(num)) < maxval
AND EXISTS (SELECT *
FROM artikli t2
WHERE sifra = num * power(10, maxval - floor(log10(num)) - 1))
AND NOT EXISTS (SELECT *
FROM artikli t2
WHERE t2.sifra = t1.sifra + 1)
AND t1.sifra >= num * power(10, maxval - floor(log10(num)) - 1) - 1
AND t1.sifra < num * power(10, maxval - floor(log10(num)) - 1) + power(10, maxval - floor(log10(num)) - 1) - 1;
END;
DB Fiddle
And if you're dealing with numbers you should use an appropriate type, not char.

Extracting more than one date from a text field

I have a SQL query that I am using that works perfectly for what I am needing at extracting the first date in a text field. This is a free form text field that is tied to a status that when the right conditions align my query looks for a date and if it is in the correct format it extracts this date.
Sometimes the date will be input as a range of dates or a comma separated list of dates. I would like to know if there is a way to extract the last date in the case of a date range or the other dates in a list of dates?
The current query has 3 steps in temp tables for extracting the date here are snippets for each step.
In the first step it looks for the word 'proposed' and grabs a number of characters after:
,SUBSTRING(al.Comments,
PATINDEX('%proposed%',al.Comments)+9,17) [DateFirstPass]
In the second step the query it extracts the date:
,LEFT(
SUBSTRING(p1.DateFirstPass, PATINDEX('%[0-9/]%', p1.DateFirstPass), 10), --string (only numbers and forward slash) MAX of 10 chars (mm/dd/yyyy)
PATINDEX('%[^0-9/]%', SUBSTRING(p1.DateFirstPass, PATINDEX('%[0-9/]%', p1.DateFirstPass), 10) + 'X')-1) --char length after negating nonvalid characters '%[^0-9/]%'
[DateSecondPass]
In the last pass it adds in the year if it is missing:
CASE
WHEN ISDATE(p2.DateSecondPass) = 1
THEN CAST(p2.DateSecondPass AS DATE)
WHEN ISDATE(p2.DateSecondPass + '/' + this.yr) = 1
THEN p2.DateSecondPass + '/' + this.yr --Adds missing year
END [DateThirdPass]
FROM
#ProposedDateParse2 p2
CROSS JOIN (VALUES (CAST(YEAR(GETDATE()) AS varchar(4)))) this(yr)
Grab a copy of patternSplitCM
Here's the code:
-- PatternSplitCM will split a string based on a pattern of the form
-- supported by LIKE and PATINDEX
--
-- Created by: Chris Morris 12-Oct-2012
CREATE FUNCTION dbo.PatternSplitCM
(
#List VARCHAR(8000) = NULL,
#Pattern VARCHAR(50)
) RETURNS TABLE WITH SCHEMABINDING
AS
RETURN
WITH numbers AS (
SELECT TOP(ISNULL(DATALENGTH(#List), 0))
n = ROW_NUMBER() OVER(ORDER BY (SELECT NULL))
FROM
(VALUES (0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) d (n),
(VALUES (0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) e (n),
(VALUES (0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) f (n),
(VALUES (0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) g (n))
SELECT
ItemNumber = ROW_NUMBER() OVER(ORDER BY MIN(n)),
Item = SUBSTRING(#List,MIN(n),1+MAX(n)-MIN(n)),
[Matched]
FROM (
SELECT n, y.[Matched], Grouper = n - ROW_NUMBER() OVER(ORDER BY y.[Matched],n)
FROM numbers
CROSS APPLY (
SELECT [Matched] = CASE WHEN SUBSTRING(#List,n,1) LIKE #Pattern THEN 1 ELSE 0 END
) y
) d
GROUP BY [Matched], Grouper
Solution against a variable:
DECLARE #string varchar(8000) =
'Blah blah 3/1/2017, 12/19/2018,1/2/2020,1111/11111/1111/111/111 blah blah';
SELECT TOP (1) Item
FROM dbo.patternSplitCM(#string, '[0-9/]')
WHERE [Matched] = 1 AND ISDATE(item) = 1
ORDER BY -ItemNumber;
Results:
item
------
1/2/2020
Example against a table:
DECLARE #table table (someid int identity, sometext varchar(1000));
INSERT #table(sometext) VALUES
('Blah blah 3/1/2017, 12/19/2018,1/2/2020,1111/11111/1111/111/111 blah blah'),
('Yada yada 1/1/12, 12/31/1999 call me at 555-1212!');
SELECT t.someid, getLastDate.Item
FROM #table t
CROSS APPLY
(
SELECT TOP (1) Item
FROM dbo.patternSplitCM(t.sometext, '[0-9/]')
WHERE [Matched] = 1 AND ISDATE(item) = 1
ORDER BY -ItemNumber
) getLastDate;
Results:
someid Item
----------- -----------
1 1/2/2020
2 12/31/1999

Does Oracle have any equivalent function for mysql "FIELD"

This is my MySQL query. How can I run this query in Oracle? Right now it's not working.
SELECT * FROM mytable WHERE id IN (1,2,3,4) ORDER BY FIELD(id,3,2,1,4);
Here is a stupid solution: (:-D)
SELECT * FROM mytable WHERE id IN (1,2,3,4)
ORDER BY CASE id WHEN 3 THEN 1
WHEN 2 THEN 2
WHEN 1 THEN 3
WHEN 4 THEN 4 END;
Or use DECODE function:
SELECT * FROM mytable WHERE id IN (1,2,3,4)
ORDER BY DECODE(id, 3, 1, 2, 2, 1, 3, 4, 4, 0)
Oracle does not have a FIELD function but you can create your own instead
create TYPE T_VAR IS TABLE OF varchar2(4000);
/
create or replace function FIELD(p_val varchar2, p_var t_var) return number is
begin
for i in 1..p_var.count
loop
if p_val = p_var(i) then
return i;
end if;
end loop;
return p_var.count + 1;
end FIELD;
/
Query
SELECT t.*
FROM mytable t
WHERE id IN (1, 2, 3, 4)
order by FIELD(id,T_VAR(3,2,1,4))
NOTE: You should create one more function FIELD with this signature FIELD (p_val number, p_var t_var) for explicit data type conversation. Function before is universal but can be cause of some bizarre effects.

mysql + all numbers from one field to Sum

Is it possible to sum the digits in a string and sort by that?
Example values: 19, 21
19 Should be transformed to 10. Explanation: 1+9=10
21 Should be transformed to 3. Explanation: 2+1= 3
After calculating these results, the table needs to be sorted by the resulting values (using SORT BY).
Originally, I have those values stored as JSON array, so it's ["1","9"] and ["2","1"], and in order to parse the JSON I'm using replace as follows:
REPLACE(REPLACE(REPLACE(item_qty, '["', ''), '"]', ''), '","', '')
How about trying something like:
SELECT (
SUBSTRING('["1","9"]', 3, 3) +
SUBSTRING('["1","9"]', 7, 7)
) AS sumOfDigits;
And then if the value ["1","9"] is stored in a column named json and the table is named table you van do:
SELECT * FROM (
SELECT table.*, (
SUBSTRING('json', 3, 3) +
SUBSTRING('json', 7, 7)
) AS sumOfDigits
FROM table
) tmp
ORDER BY sumOfDigits;
I would define a function to do the sum, as follow:
DELIMITER //
CREATE FUNCTION add_digits
(
number INTEGER
) RETURNS INTEGER
BEGIN
DECLARE my_sum INTEGER;
SET my_sum = 0;
SET number = ABS(number);
WHILE (number > 0) DO
SET my_sum = my_sum + (number MOD 10);
SET number = number DIV 10;
END WHILE;
RETURN my_sum;
END //
DELIMITER ;
You could also create a function that works directly on your json sting, parsing it for digits and adding their values.