Text transformation: Adding text according to previous occurrence - php

I have a text with multiple questions in the following format:
Q1
Question text 1?
1. Answer A
2. Answer B (+1p)
3. Answer C
4. Answer D
Q2
Question Text 2?
1. Answer A (+1p)
2. Answer B
3. Answer C (+1p)
4. Answer D
Q3
Question Text 3
1. Answer A
2. Answer B
3. Answer C (+1p)
Correct answers are marked with (+1p).
I'd like to reformat it so that the correct answers are stated in a new line like below:
Q1
Question text 1?
1. Answer A
2. Answer B
3. Answer C
4. Answer D
Answer: B
Q2
Question Text 2?
1. Answer A
2. Answer B
3. Answer C
4. Answer D
Answer: A, C
Q3
Question Text 3
1. Answer A
2. Answer B
3. Answer C
Answer: C
Is this even possible to accomplish in Notepad++?

The magic of regular expression to the rescue:
We need a two step approach,
Append Answer:
Find What: ((\R\d\.\h+Answer\h+[A-Z]+\h?(\(\+1p\))?)+)
Replace With: \1\r\nAnswer:
Check Regular Expression
Click Replace or Replace All
Now we collect the answers:
Find What: Answer ([A-Z])\h\(\+1p\)(.*?Answer: [A-Z ]*)
Replace With: Answer \1\2\1
Check Regular Expression
Check . matches newline
Click Replace or Replace All. Keep clicking, the case with several answers in one block needs as much Replace All as there are answers in the block. Observe the message in the dialogs status bar. It will tell you when you are done.
In the first step, the find tries to match a complete block of answers and capture it in \1. The replacement add just a line after the block.
The second step tries (for each block) to capture the lines from the first (+1p) up to the Answer:. The find is such that (+1p) is not cpatured. The answer char of the answer is captured in \1, the following answers up until the Answer: line are captured in \2 and we append the answer char in '\1' to the 'Answer:' line. (Just do a few finds, to see what is matched, then do a few Replaces to see how it works with block that have several marked answers. You can Undo to replay a replace.)

Sometimes a question thrills you here on SO (aka this can be done somehow...)
As of now, you may have come to the conclusion that this is no easy task for an editor like Notepad++ alone (if not impossible at all), so I thought about a solution in a programming language (in my case PHP with the help of regular expressions) and would like to present it here:
Explanation:
What the code basically does are the following steps:
Look for questions blocks - these are blocks of lines beginning with a digit and a dot, sourrounded by empty lines on each site - and save theirs positions in the original string.
In these lines, try to find marked answers (the pattern (+1p))
Create a new string with the possible answers
The position where the answer string (Answer: ...) needs to be inserted can be calculated by the following equation:
(original offset) + strlen(original string) + strlen(answer_string)
Code:
<?php
$string = 'your original string here';
$regex_questions = '~(?ms)(?:^$\R)(?P<answers>(?:^\d\. Answer [A-E].*?\R)+?)(?:^$\R)(?-ms)~';
# does what is described in point 1.)
preg_match_all($regex_questions, $string, $questions, PREG_OFFSET_CAPTURE);
$regex_answers = '~(?m)^(?:\d\. Answer (?<choice>[A-E]).*?\(\+1p\))$~';
# point 2.)
$offset = 0;
# loops over the questions
foreach ($questions["answers"] as $question) {
preg_match_all($regex_answers, $question[0], $answers);
$answer = "Answer: " . implode(',', $answers["choice"]) ."\n";
# point 3.)
$position = $offset + $question[1] + strlen($question[0]);
# point 4.)
$string = substr_replace($string, $answer, $position, 0);
$offset += strlen($answer);
}
echo $string;
# After every code block there's a string with the appropiate answers
?>
Demo:
Find an online demo on ideone.

Related

Matching string that contains asterisk [duplicate]

This question already has answers here:
Reference - What does this regex mean?
(1 answer)
Regular expressions: Ensuring b doesn't come between a and c
(4 answers)
Closed 3 years ago.
I know this sounds easy but I am stuck.
I want to match strings that has asterisk *.
Essentially I want to allow strings having asterisk at front/back/both but not middle:
(At max there will be 2 asterisks, front and both but no middle, and the presence string is a must)
ALLOW:
*string* *string string* string
DENY:
*str*ing*
*str*ing str*ing* str*ing
*string*****
I tried
^\\*?((?!\\*).)*\\\*?$
and somehow it works.
Can someone explains how this works?
And verify if this is correct because regex..hard to debug and check..
You can use the following regex:
^\*?\w+\*?$
demo: https://regex101.com/r/vwuXv2/1/
Explanations:
^ anchor imposing the start of a line
\*? a * appearing at most one time
\w+ at least 1 word char appearing in the text ([a-zA-Z0-9_] feel free to change it depending on your need)
\*? a * appearing at most one time
$ end of line anchor
Now if you are interested in partial line matches, you can use the following regex:
(?<=^| )\*?\w+\*?(?=$| )
demo: https://regex101.com/r/vwuXv2/2/
Explanations: you add lookbehind, lookahead assertions.
Adding Japanese characters as requested in the comment (add in [^*\s] all the characters you need to exclude from the words):
^\*?[^*\s]+\*?$
demo: https://regex101.com/r/RaCmwt/1/
or
^\*?[[:alpha:]]+\*?$
(with unicode flag enabled) or just
^\*?\p{L}+\*?$
demo: https://regex101.com/r/RaCmwt/2/
You can simply say: Optionally start with asterisk, 0 or more arbitrary characters except asterisk, optionally end with asterisk.
^\*?[^*]*\*?$
https://regex101.com/r/bibCEc/2
An alternative is to inverse the match and test if there is not ( i.e. if(!...)) any asterisk not at the begin or end using negative look behind and look ahead:
(?<!^)\*(?!$)
https://regex101.com/r/8St0M4/2
According to your recent edit you would use the quatifier + to match 1 or more characters:
^\*?[^*]+\*?$
https://regex101.com/r/bibCEc/3

PHP PREG_SPLIT on numbers 1-100

I am working on some code to break down the full text of a test that will copied and pasted with the following format:
1. This is question number one.
A. Answer 1
B. Answer 2
C. Answer 3
D. Answer 4
2. This is question number two.
3. This is another question, number three.
45. Ken has uses his money, $353. How much does he have after spending $214.
I am using the following preg_split:
$questions = preg_split("/[0-9]+\./", $_POST[test]);
My problem has come in with questions like #45 where there are numbers in the question itself and they are followed by a period.
I just want to match the numbers 1-100 followed by a period. Eg.
1.
2.
3.
4.
5.
etc
I think it is better to use multiline flag with ^:
$questions = preg_split('/^ *[0-9]+\. +/m', $_POST[test]);
A number between 1 and 100, followed by a period, can be matched by
/\b(?:100|[1-9][0-9]?)\./
but if the actual rule is to match a number at the start of a line, use
/^\d+\./m
You can use preg_match_all() instead:
preg_match_all('~(?:^|\R)[0-9]+\. \K.+~', $_POST['test'], $matches);
$questions = $matches[0];
Use ^ to specify that it's the beginning of the line, using the g and m modifiers to specify global and multiline:
/^[0-9]+\.\s/m

Match just once with regex

I'm using this regex to mach some words without numbers and it works well
(?:searchForThis|\G).+?(\b[^\d\s]+?\b)
The problem that Regex searching the entire document and not only in the line that contains searchForThis
So if I have 2 times searchForThis it will take them twice
I want to stop it only on that 1st line so it will not search the other lines after
Any help please?
I'm using Regex with php
Example of the problem here: http://www.rubular.com/r/vPhk8VbqZR
In the example you will see :
Match 1
1. word
Match 2
1. worldtwo
Match 3
1. wordfive
Match 4
1. word
Match 5
1. worldtwo
Match 6
1. wordfive
But I need only :
Match 1
1. word
Match 2
1. worldtwo
Match 3
1. wordfive
You will see that it's doing twice
===========Edit for more details as asked ===========================
In my php I have :
define('CODE_REGEX', '/(?:searchForThis|\G(?<!^)).*?(\b[a-zA-Z]+\b)/iu')
Output :
if (preg_match_all(CODE_REGEX, $content, $result))
return trim($result[1][0].' '.$result[1][1].' '.$result[1][2].' '.$result[1][3].' '.$result[1][4].' '.$result[1][5]);
Thank you
You can use this pattern instead:
(?:\A[\s\S]*?searchForThis|\G).*?(\b[a-z]+\b)/iu
or
(?:\A(?s).*?searchForThis|\G)(?-s).*?(\b[a-z]+\b)/iu
To deal with multiple line between the first "searchForThis" and others or the end of the string, you can use this: (with your example string you will obtain "After" and "this".)
(?:\A.*?searchForThis|\G)(?>[^a-z]++|\b[a-z]++\S)*?(?!searchForThis)(\b[a-z]+\b)/ius
Note: in all the three pattern you can replace \A with ^ since the multiline mode is not used. Be carefull with rubular that is designed for ruby regexes: m in ruby = s in php (that is the dotall/singleline mode), m in php is the multiline mode (each start of the line can be matched with ^)
You can make it in two stages :
// get the first line with 'searchForThis'
preg_match('/searchForThis(?<line>.*)\n/m', $text, $results);
$line = $results['line'];
// get every word from this line
preg_match_all('/\b[a-z]+\b/i', $line, $results);
$words = $results[0];
Another way, based on the great Casimir's answer (just for readibility) :
preg_match_all('/(?s:^.*?searchForThis|\G).*?(?<words>\b[a-z]+\b)/iu', $str, $results);
$words = $results['words'];

PHP: preg_replace to match some numbers but not others

So I've been working on a little project to write a syntax highlighter for a game's scripting language. It's all gone off without a hitch, except for one part: the numbers.
Take these lines for example
(5:42) Set database entry {healthpoints2} to the value 100.
(5:140) Move the user to position (29,40) on the map.
I want to highlight that 100 on the end, without highlighting the (5:42) or the 2 in the braces. The numbers won't always be in the same place, and there won't always only be one number.
I basically need a regexp to say:
"Match any numbers that aren't anywhere between {} and don't match the (#:#) pattern."
I've been at this for a day and a half now and I'm pulling out my hair trying to figure it out. Help with this would be greatly appreciated!
I've already looked at regular-expressions.info, and tried playing around with RegexBuddy, but i'm just not getting it :c
Edit: By request, here's some more lines copied right from the script editor.
(0:7) When somebody moves into position (**10** fhejwkfhwjekf **20**,
(0:20) When somebody rolls exactly **10** on **2** dice of **6** sides,
(0:31) When somebody says {...},
(3:3) within the diamond (**5**,**10**) - **20** //// **25**,
(3:14) in a line starting at (#, #) and going # more spaces northeast.
(5:10) play sound # to everyone who can see (#,#).
(5:14) move the user to (#,#) if there's nobody already there.
(5:272) set message ~msg to be the portion of message ~msg from position # to position #.
(5:302) take variable %var and add # to it.
(5:600) set database entry {...} about the user to #.
(5:601) set database entry {...} about the user named {...} to #.
You might kick yourself when you see this solution...
Assuming this desired number will always be used in a sentence, it should always have a space preceding it.
$pattern = '/ [0-9]+/s';
If the preceding space isn't always present, let me know and I'll update the answer.
Here's the updated regex to match the 2 examples in your question:
$pattern = '/[^:{}0-9]([0-9,]+)[^:{}0-9]/s';
3nd update to account for your question revisions:
$pattern = '/[^:{}0-9a-z#]([0-9]+[, ]?[0-9]*)[^:{}0-9a-z#]/s';
So you don't highlight the number in things like
{update 29 testing}
you might want to pre strip the braces, like so:
$pattern = '/[^:{}0-9a-z#]([0-9]+[, ]?[0-9]*)[^:{}0-9a-z#]/s';
$str = '(0:7) Hello {update 29 testing} 123 Rodger alpha charlie 99';
$tmp_str = preg_replace('/{[^}]+}/s', '', $str);
preg_match($pattern, $tmp_str, $matches);
(\d+,\s?\d+)|(?<![\(\{:]|:\d)\d+(?![\)\}])
http://regexr.com?30omd
Would this work?

PHP Regex Check if two strings share two common characters

I'm just getting to know regular expressions, but after doing quite a bit of reading (and learning quite a lot), I still have not been able to figure out a good solution to this problem.
Let me be clear, I understand that this particular problem might be better solved not using regular expressions, but for the sake of brevity let me just say that I need to use regular expressions (trust me, I know there are better ways to solve this).
Here's the problem. I'm given a big file, each line of which is exactly 4 characters long.
This is a regex that defines "valid" lines:
"/^[AB][CD][EF][GH]$/m"
In english, each line has either A or B at position 0, either C or D at position 1, either E or F at position 2, and either G or H at position 3. I can assume that each line will be exactly 4 characters long.
What I'm trying to do is given one of those lines, match all other lines that contain 2 or more common characters.
The below example assumes the following:
$line is always a valid format
BigFileOfLines.txt contains only valid lines
Example:
// Matches all other lines in string that share 2 or more characters in common
// with "$line"
function findMatchingLines($line, $subject) {
$regex = "magic regex I'm looking for here";
$matchingLines = array();
preg_match_all($regex, $subject, $matchingLines);
return $matchingLines;
}
// Example Usage
$fileContents = file_get_contents("BigFileOfLines.txt");
$matchingLines = findMatchingLines("ACFG", $fileContents);
/*
* Desired return value (Note: this is an example set, there
* could be more or less than this)
*
* BCEG
* ADFG
* BCFG
* BDFG
*/
One way I know that will work is to have a regex like the following (the following regex would only work for "ACFG":
"/^(?:AC.{2}|.CF.|.{2}FG|A.F.|A.{2}G|.C.G)$/m"
This works alright, performance is acceptable. What bothers me about it though is that I have to generate this based off of $line, where I'd rather have it be ignorant of what the specific parameter is. Also, this solution doesn't scale terrible well if later the code is modified to match say, 3 or more characters, or if the size of each line grows from 4 to 16.
It just feels like there's something remarkably simple that I'm overlooking. Also seems like this could be a duplicate question, but none of the other questions I've looked at really seem to address this particular problem.
Thanks in advance!
Update:
It seems that the norm with Regex answers is for SO users to simply post a regular expression and say "This should work for you."
I think that's kind of a halfway answer. I really want to understand the regular expression, so if you can include in your answer a thorough (within reason) explanation of why that regular expression:
A. Works
B. Is the most efficient (I feel there are a sufficient number of assumptions that can be made about the subject string that a fair amount of optimization can be done).
Of course, if you give an answer that works, and nobody else posts the answer *with* a solution, I'll mark it as the answer :)
Update 2:
Thank you all for the great responses, a lot of helpful information, and a lot of you had valid solutions. I chose the answer I did because after running performance tests, it was the best solution, averaging equal runtimes with the other solutions.
The reasons I favor this answer:
The regular expression given provides excellent scalability for longer lines
The regular expression looks a lot cleaner, and is easier for mere mortals such as myself to interpret.
However, a lot of credit goes to the below answers as well for being very thorough in explaining why their solution is the best. If you've come across this question because it's something you're trying to figure out, please give them all a read, helped me tremendously.
Why don't you just use this regex $regex = "/.*[$line].*[$line].*/m";?
For your example, that translates to $regex = "/.*[ACFG].*[ACFG].*/m";
This is a regex that defines "valid" lines:
/^[A|B]{1}|[C|D]{1}|[E|F]{1}|[G|H]{1}$/m
In english, each line has either A or B at position 0, either C or D
at position 1, either E or F at position 2, and either G or H at
position 3. I can assume that each line will be exactly 4 characters
long.
That's not what that regex means. That regex means that each line has either A or B or a pipe at position 0, C or D or a pipe at position 1, etc; [A|B] means "either 'A' or '|' or 'B'". The '|' only means 'or' outside of character classes.
Also, {1} is a no-op; lacking any quantifier, everything has to appear exactly once. So a correct regex for the above English is this:
/^[AB][CD][EF][GH]$/
or, alternatively:
/^(A|B)(C|D)(E|F)(G|H)$/
That second one has the side effect of capturing the letter in each position, so that the first captured group will tell you whether the first character was A or B, and so on. If you don't want the capturing, you can use non-capture grouping:
/^(?:A|B)(?:C|D)(?:E|F)(?:G|H)$/
But the character-class version is by far the usual way of writing this.
As to your problem, it is ill-suited to regular expressions; by the time you deconstruct the string, stick it back together in the appropriate regex syntax, compile the regex, and do the test, you would probably have been much better off just doing a character-by-character comparison.
I would rewrite your "ACFG" regex thus: /^(?:AC|A.F|A..G|.CF|.C.G|..FG)$/, but that's just appearance; I can't think of a better solution using regex. (Although as Mike Ryan indicated, it would be better still as /^(?:A(?:C|.E|..G))|(?:.C(?:E|.G))|(?:..EG)$/ - but that's still the same solution, just in a more efficiently-processed form.)
You've already answered how to do it with a regex, and noted its shortcomings and inability to scale, so I don't think there's any need to flog the dead horse. Instead, here's a way that'll work without the need for a regex:
function findMatchingLines($line) {
static $file = null;
if( !$file) $file = file("BigFileOfLines.txt");
$search = str_split($line);
foreach($file as $l) {
$test = str_split($l);
$matches = count(array_intersect($search,$test));
if( $matches > 2) // define number of matches required here - optionally make it an argument
return true;
}
// no matches
return false;
}
There are 6 possibilities that at least two characters match out of 4: MM.., M.M., M..M, .MM., .M.M, and ..MM ("M" meaning a match and "." meaning a non-match).
So, you need only to convert your input into a regex that matches any of those possibilities. For an input of ACFG, you would use this:
"/^(AC..|A.F.|A..G|.CF.|.C.G|..FG)$/m"
This, of course, is the conclusion you're already at--so good so far.
The key issue is that Regex isn't a language for comparing two strings, it's a language for comparing a string to a pattern. Thus, either your comparison string must be part of the pattern (which you've already found), or it must be part of the input. The latter method would allow you to use a general-purpose match, but does require you to mangle your input.
function findMatchingLines($line, $subject) {
$regex = "/(?<=^([AB])([CD])([EF])([GH])[.\n]+)"
+ "(\1\2..|\1.\3.|\1..\4|.\2\3.|.\2.\4|..\3\4)/m";
$matchingLines = array();
preg_match_all($regex, $line + "\n" + $subject, $matchingLines);
return $matchingLines;
}
What this function does is pre-pend your input string with the line you want to match against, then uses a pattern that compares each line after the first line (that's the + after [.\n] working) back to the first line's 4 characters.
If you also want to validate those matching lines against the "rules", just replace the . in each pattern to the appropriate character class (\1\2[EF][GH], etc.).
People may be confused by your first regex. You give:
"/^[A|B]{1}|[C|D]{1}|[E|F]{1}|[G|H]{1}$/m"
And then say:
In english, each line has either A or B at position 0, either C or D at position 1, either E or F at position 2, and either G or H at position 3. I can assume that each line will be exactly 4 characters long.
But that's not what that regex means at all.
This is because the | operator has the highest precedence here. So, what that regex really says, in English, is: Either A or | or B in the first position, OR C or | or D in the first position, OR E or | or F in the first position, OR G or '|orH` in the first position.
This is because [A|B] means a character class with one of the three given characters (including the |. And because {1} means one character (it is also completely superfluous and could be dropped), and because the outer | alternate between everything around it. In my English expression above each capitalized OR stands for one of your alternating |'s. (And I started counting positions at 1, not 0 -- I didn't feel like typing the 0th position.)
To get your English description as a regex, you would want:
/^[AB][CD][EF][GH]$/
The regex will go through and check the first position for A or B (in the character class), then check C or D in the next position, etc.
--
EDIT:
You want to test for only two of these four characters matching.
Very Strictly speaking, and picking up from #Mark Reed's answer, the fastest regex (after it's been parsed) is likely to be:
/^(A(C|.E|..G))|(.C(E)|(.G))|(..EG)$/
as compared to:
/^(AC|A.E|A..G|.CE|.C.G|..EG)$/
This is because of how the regex implementation steps through text. You first test if A is in the first position. If that succeeds, then you test the sub-cases. If that fails, then you're done with all those possible cases (or which there are 3). If you don't yet have a match, you then test if C is in the 2nd position. If that succeeds, then you test for the two subcases. And if none of those succeed, you test, `EG in the 3rd and 4th positions.
This regex is specifically created to fail as fast as possible. Listing each case out separately, means to fail, you would have test 6 different cases (each of the six alternatives), instead of 3 cases (at a minimum). And in cases of A not being the first position, you would immediately go to test the 2nd position, without hitting it two more times. Etc.
(Note that I don't know exactly how PHP compiles regex's -- it's possible that they compile to the same internal representation, though I suspect not.)
--
EDIT: On additional point. Fastest regex is a somewhat ambiguous term. Fastest to fail? Fastest to succeed? And given what possible range of sample data of succeeding and failing rows? All of these would have to be clarified to really determine what criteria you mean by fastest.
Here's something that uses Levenshtein distance instead of regex and should be extensible enough for your requirements:
$lines = array_map('rtrim', file('file.txt')); // load file into array removing \n
$common = 2; // number of common characters required
$match = 'ACFG'; // string to match
$matchingLines = array_filter($lines, function ($line) use ($common, $match) {
// error checking here if necessary - $line and $match must be same length
return (levenshtein($line, $match) <= (strlen($line) - $common));
});
var_dump($matchingLines);
I bookmarked the question yesterday in the evening to post an answer today, but seems that I'm a little late ^^ Here is my solution anyways:
/^[^ACFG]*+(?:[ACFG][^ACFG]*+){2}$/m
It looks for two occurrences of one of the ACFG characters surrounded by any other characters. The loop is unrolled and uses possessive quantifiers, to improve performance a bit.
Can be generated using:
function getRegexMatchingNCharactersOfLine($line, $num) {
return "/^[^$line]*+(?:[$line][^$line]*+){$num}$/m";
}

Categories