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
Related
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
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
;
I'm trying to construct a function that generates the row and cell HTML tags which will be used in another application to build a basic HTML table. The row and cells need to be grouped/concatenated by the region id. This is the part I'm currently struggling with.
I've tried to put the following function together, but not really sure where to go to ensure that the output is grouped correctly by the region_id.
This is not something I would generally do SQL, but I'm working with some limited technologies.
create table reporting
(
id integer,
region_id integer,
category text,
item text,
status text
);
insert into reporting values
(1, 1, 'audio', 'speakers', 'delivered'),
(2, 1, 'display', 'monitors', 'pending'),
(3, 2, 'cables', 'hdmi', 'pre-order'),
(4, 3, 'storage', 'sdd', 'cancelled'),
(5, 3, 'software', 'business', 'delivered'),
(6, 3, 'other', 'support', 'delivered');
create function html_out (query text)
returns TABLE(region_id text, result text) language plpgsql as $$
declare
rec record;
header boolean := true;
begin
for rec in
execute format($q$
select row_to_json(q) json_row
from (%s) q
$q$, query)
loop
return query select region_id,
format ('<tr><td>%s</td></tr>', string_agg(value, '</td><td>'))
from json_each_text(rec.json_row);
end loop;
end $$;
select html_out('select region_id, category, item, status from reporting');
You can use string aggregation. I think the logic you want is:
select
region_id,
'<tr><td>'
|| string_agg(concat_ws('</td><td>', category, item, status), '</td></tr><tr><td>')
|| '</td></tr>' html
from reporting
group by region_id
order by 1
Demo on DB Fiddle
region_id | html
--------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
1 | <tr><td>audio</td><td>speakers</td><td>delivered</td></tr><tr><td>display</td><td>monitors</td><td>pending</td></tr>
2 | <tr><td>cables</td><td>hdmi</td><td>pre-order</td></tr>
3 | <tr><td>storage</td><td>sdd</td><td>cancelled</td></tr><tr><td>software</td><td>business</td><td>delivered</td></tr><tr><td>other</td><td>support</td><td>delivered</td></tr>
If this is about consolidating repeating region_id with rowspan=, then you can get that in one go like this:
with consolidate as (
select region_id, concat('<td rowspan="', count(*), '">') as rowspan
from reporting
group by region_id
), trows as (
select row_number() over (order by r.region_id, r.id) as rnum,
concat(
'<tr>',
case
when lag(c.region_id) over w = c.region_id then ''
else concat(c.rowspan, c.region_id, '</td>')
end,
'<td>',
array_to_string(array[r.category, r.item, r.status]::text[], '</td><td>', '</td>'),
'</tr>'
) as html
from consolidate c
join reporting r on r.region_id = c.region_id
window w as (partition by r.region_id order by r.id)
)
select array_to_string(array_agg(html order by rnum), '
')
from trows;
-[ RECORD 1 ]---+-----------------------------------------------------------------------------
array_to_string | <tr><td rowspan="2">1</td><td>audio</td><td>speakers</td><td>delivered</tr> +
| <tr><td>display</td><td>monitors</td><td>pending</tr> +
| <tr><td rowspan="1">2</td><td>cables</td><td>hdmi</td><td>pre-order</tr> +
| <tr><td rowspan="3">3</td><td>storage</td><td>sdd</td><td>cancelled</tr> +
| <tr><td>software</td><td>business</td><td>delivered</tr> +
| <tr><td>other</td><td>support</td><td>delivered</tr>
I put a newline in as the delimiter for that last array_to_string() just to get it to look nice. A ' ' or '' would probably be what you want.
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.
I have a table with data representing a tree structure, with one column indicating the row's position in the hierarchical tree. Each level is separated with a -.
1
1-1
2
2-1
2-2
2-2-1
2-2-2
2-2-2-1
The tree is retrieved in order simply with an ORDER BY on this column. This falls down when there are more than 10 items at any level, as the column is sorted alphabetically. MySQL sorts 10 before 3.
Actual result:
1
1-10
1-3
2
Desired result:
1
1-3
1-10
2
There could be any number of levels of depth to the values.
Is it possible to sort this data numerically in MySQL?
I think your best shot is to convert the data into something that does naturally sort. If you tree structure will always have less than 99 children, you could create a function like I have below. You would just use the "GetTreeStructureSort(columnName)" in the sort function. (If you have the possibility of 3-digit numbers, you could adjust this to be more intuitive.)
CREATE FUNCTION GetTreeStructureSort
(
-- Add the parameters for the function here
#structure varchar(500)
)
RETURNS varchar(500)
AS
BEGIN
DECLARE #sort varchar(500)
-- Add a hyphen to the beginning and end to make all the numbers from 1 to 9 easily replaceable
SET #sort = '-' + #structure + '-'
-- Replace each instance of a one-digit number to a two-digit representation
SELECT #sort = REPLACE(#sort, '-1-', '-01-')
SELECT #sort = REPLACE(#sort, '-2-', '-02-')
SELECT #sort = REPLACE(#sort, '-3-', '-03-')
SELECT #sort = REPLACE(#sort, '-4-', '-04-')
SELECT #sort = REPLACE(#sort, '-5-', '-05-')
SELECT #sort = REPLACE(#sort, '-6-', '-06-')
SELECT #sort = REPLACE(#sort, '-7-', '-07-')
SELECT #sort = REPLACE(#sort, '-8-', '-08-')
SELECT #sort = REPLACE(#sort, '-9-', '-09-')
-- Strip off the first and last hyphens that were added at the beginning.
SELECT #sort = SUBSTRING(#sort, 2, LEN(#sort) - 2)
-- Return the result of the function
RETURN #sort
END
This would convert these results:
1
1-10
1-3
2
into this:
01
01-03
01-10
02
I tested this with the following code:
DECLARE #something varchar(255)
set #something = '1-10-3-21'
SELECT dbo.GetTreeStructureSort(#something)