Get last 3 characters during user typing - wpf

I need get last three character from richTextBox during when the user writes some text in richTextbox.
I bind property on Text property of richTextBox from Extended WPF toolkit.
public string RtbText
{
get { return _rtbText; }
set
{
_rtbText = value;
NotifyPropertyChanged("RtbText");
}
}
I use Reactive Extensions for .NET (Rx) and make Observer on property RtbText
Observable.FromEvent<PropertyChangedEventArgs>(this, "PropertyChanged")
.Where(e => e.EventArgs.PropertyName == "RtbText")
.Select(_ => this.RtbText)
.Where(text => text.Length > 1)
.Do(AddSmiles)
.Throttle(TimeSpan.FromSeconds(1))
.Subscribe(GetLastThreeChars);
private void GetLastThreeChars(string text)
{
if (text.Length > 3)
{
string lastThreeChars = text.Substring(text.Length - 2, text.Length);
}
}
But if I start typing in richTextBox I get this exception:
Index and length must refer to a location within the string.
Parameter name: length
at System.String.InternalSubStringWithChecks(Int32 startIndex, Int32 length, Boolean fAlwaysCopy)
at System.String.Substring(Int32 startIndex, Int32 length)
at WpfApplication1.MainWindow.GetLastThreeChars(String text) in C:\Users\Jan\Documents\Visual Studio 2010\Projects\C#\Pokec_Messenger\ver.beta\IoC.Get\Pokec_Messenger\ver.beta\Pokec_Messenger\WpfApplication1\MainWindow.xaml.cs:line 97
at System.Linq.Observable.<>c_DisplayClass389`1.<>c_DisplayClass38b.b_388(TSource x)

If text.Length > 3 (say it's 4) then:
text.Length - 2 = 2
So you're code is:
string lastThreeChars = text.Substring(2, 4);
This will fail as you are asking for four characters in the substring, which puts it out of range.
String.Substring Method (Int32, Int32)
Retrieves a substring from this instance. The substring starts at a specified character position and has a specified length.
Additionally your test and starting position are incorrect. Don't forget that C# arrays and strings are zero indexed. Checking for the case of length strictly greater than 3 you'll miss the case when the user has entered exactly three characters when you want to return the entire string.
Your code needs to be:
if (text.Length > 2)
{
string lastThreeChars = text.Substring(text.Length - 3, 3);
}
If fact you don't need to specify the length:
if (text.Length > 2)
{
string lastThreeChars = text.Substring(text.Length - 3);
}
will return the last three characters in the string.

This is another form. Which gets all the characters from some start position up to the end
string lastThreeChars = text.Substring(text.Length - 3);
maybe text.Length - 2. Untested

Related

How do I setSelection after a text search?

I am attempting to add a search feature to Quill and want to highlight the text that it finds. I am having trouble getting the range index for the text it finds, and it is likely that I am not using the correct approach.
So far, I get the text using getContents() and can find matches by iterating through the lines. However, I have not been able to find the correct index position for setSelection(). A second issue is that I want the window to scroll to where the selection is found, and it is not scrolling into view.
...
myKeyPhrases = ['obvious', 'orange', 'apple'];
var myDelta = editor.getContents();
myDelta.eachLine((line, attributes, i) => {
mytext = line.filter((op) => typeof op.insert === 'string').map((op) => op.insert).join('');
mytext = mytext.toLowerCase();
ndx = isKeyPhraseFound(mytext, myKeyPhrases);
if (ndx >= 0){
// The code finds the matches OK to here.
// The next 4 lines don't get the correct range and scroll.
index = i;
editor.focus();
editor.setSelection(index, 1, Quill.sources.USER);
editor.scrollIntoView();
return index;
}
});
My desired results would be that a found text match would be selected and that the window would be scrolled so that the selection is displayed. The actual results are that the wrong text is selected and the window is not scrolled to view the selection.
You're basically there. I suspect your issue is that you're filtering out block elements, which acts similarly to Quill's getText method. According to the docs:
Non-string content are omitted, so the returned string’s length may be shorter than the editor’s as returned by getLength.
This is because non-text elements usually have a length of 1, so for every one of those you omit, your index will drift by 1.
A simple workaround for this is to replace any non-text elements with a newline character, which has length 1, and the added benefit of causing a search across it to fail (unless you let users search with newlines - maybe then you can pick another special character like a control character).
The following method should return a plain-text representation of your Quill contents whose length matches quill.getLength, and therefore should be able to be searched using whatever JavaScript text search method you want (eg indexOf for simple searches).
function textContents() {
return quill.getContents().ops
.reduce((text, op) => {
if (typeof op.insert === 'string') {
// If the op is a string insertion, just concat
return text + op.insert;
} else {
// Otherwise it's a block. Represent this as a newline,
// which will preserve the length of 1, and also prevent
// searches matching across the block
return text + '\n';
}
}, '');
}
Here's a rough example: https://codepen.io/alecgibson/pen/GLVzPb

Stream of Char to Stream of Byte/Byte Array

The following code takes a String s, converts into char array, filters digits from it, then converts it to string, then converts into byte array.
char charArray[] = s.toCharArray();
StringBuffer sb = new StringBuffer(charArray.length);
for(int i=0; i<=charArray.length-1; i++) {
if (Character.isDigit(charArray[i]))
sb.append(charArray[i]);
}
byte[] bytes = sb.toString().getBytes(Charset.forName("UTF-8"));
I'm trying to change the above code to streams approach. Following is working.
s.chars()
.sequential()
.mapToObj(ch -> (char) ch)
.filter(Character::isDigit)
.collect(StringBuilder::new,
StringBuilder::append, StringBuilder::append)
.toString()
.getBytes(Charset.forName("UTF-8"));
I think there could be a better way to do it.
Can we directly convert Stream<Character> to byte[] & skip the conversion to String in between?
First, both of your variants have the problem of not handling characters outside the BMP correctly.
To support these characters, there is codePoints() as an alternative to chars(). You can use appendCodePoint on the target StringBuilder to consistently use codepoints throughout the entire operation. For this, you have to remove the unnecessary .mapToObj(ch -> (char) ch) step, whose removal also eliminates the overhead of creating a Stream<Character>.
Then, you can avoid the conversion to a String in both cases, by encoding the StringBuilder using the Charset directly. In the case of the stream variant:
StringBuilder sb = s.codePoints()
.filter(Character::isDigit)
.collect(StringBuilder::new,
StringBuilder::appendCodePoint, StringBuilder::append);
ByteBuffer bb = StandardCharsets.UTF_8.encode(CharBuffer.wrap(sb));
byte[] utf8Bytes = new byte[bb.remaining()];
bb.get(utf8Bytes);
Performing the conversion directly with the stream of codepoints is not easy. Not only is there no such support in the Charset API, there is no straight-forward way to collect a Stream into a byte[] array.
One possibility is
byte[] utf8Bytes = s.codePoints()
.filter(Character::isDigit)
.flatMap(c -> c<128? IntStream.of(c):
c<0x800? IntStream.of((c>>>6)|0xC0, c&0x3f|0x80):
c<0x10000? IntStream.of((c>>>12)|0xE0, (c>>>6)&0x3f|0x80, c&0x3f|0x80):
IntStream.of((c>>>18)|0xF0, (c>>>12)&0x3f|0x80, (c>>>6)&0x3f|0x80, c&0x3f|0x80))
.collect(
() -> new Object() { byte[] array = new byte[8]; int size;
byte[] result(){ return array.length==size? array: Arrays.copyOf(array,size); }
},
(b,i) -> {
if(b.array.length == b.size) b.array=Arrays.copyOf(b.array, b.size*2);
b.array[b.size++] = (byte)i;
},
(a,b) -> {
if(a.array.length<a.size+b.size) a.array=Arrays.copyOf(a.array,a.size+b.size);
System.arraycopy(b.array, 0, a.array, a.size, b.size);
a.size+=b.size;
}).result();
The flatMap step converts the stream of codepoints to a stream of UTF-8 unit. (Compare with the UTF-8 description on Wikipedia) The collect step collects int values into a byte[] array.
It’s possible to eliminate the flatMap step by creating a dedicate collector which collects a stream of codepoints directly into a byte[] array
byte[] utf8Bytes = s.codePoints()
.filter(Character::isDigit)
.collect(
() -> new Object() { byte[] array = new byte[8]; int size;
byte[] result(){ return array.length==size? array: Arrays.copyOf(array,size); }
void put(int c) {
if(array.length == size) array=Arrays.copyOf(array, size*2);
array[size++] = (byte)c;
}
},
(b,c) -> {
if(c < 128) b.put(c);
else {
if(c<0x800) b.put((c>>>6)|0xC0);
else {
if(c<0x10000) b.put((c>>>12)|0xE0);
else {
b.put((c>>>18)|0xF0);
b.put((c>>>12)&0x3f|0x80);
}
b.put((c>>>6)&0x3f|0x80);
}
b.put(c&0x3f|0x80);
}
},
(a,b) -> {
if(a.array.length<a.size+b.size) a.array=Arrays.copyOf(a.array,a.size+b.size);
System.arraycopy(b.array, 0, a.array, a.size, b.size);
a.size+=b.size;
}).result();
but it doesn’t add to readability.
You can test the solutions using a String like
String s = "some test text 1234 ✔ 3 𝟝";
and printing the result as
System.out.println(Arrays.toString(utf8Bytes));
System.out.println(new String(utf8Bytes, StandardCharsets.UTF_8));
which should produce
[49, 50, 51, 52, -17, -68, -109, -16, -99, -97, -99]
12343𝟝
It should be obvious that the first variant is the simplest, and it will have reasonable performance, even if it doesn’t create a byte[] array directly. Further, it’s the only variant which can be adapted for getting other result charsets.
But even the
byte[] utf8Bytes = s.codePoints()
.filter(Character::isDigit)
.collect(StringBuilder::new,
StringBuilder::appendCodePoint, StringBuilder::append)
.toString().getBytes(StandardCharsets.UTF_8);
is not so bad, regardless of whether the toString() operation bears a copying operation.

TextEncodings.Base64Url.Decode vs Convert.FromBase64String

I was working on creating a method that would generate a JWT token. Part of the method reads a value from my web.config that services as the "secret" used to generate the hash used to create the signature for the JWT token.
<add key="MySecret" value="j39djak49H893hsk297353jG73gs72HJ3tdM37Vk397" />
Initially I tried using the following to convert the "secret" value to a byte array.
byte[] key = Convert.FromBase64String(ConfigurationManager.AppSettings["MySecret"]);
However, an exception was thrown when this line was reached ...
The input is not a valid Base-64 string as it contains a non-base 64 character, more than two padding characters, or an illegal character among the padding characters.
So I looked into the OAuth code and so another method being used to change a base64 string into a byte array
byte[] key = TextEncodings.Base64Url.Decode(ConfigurationManager.AppSettings["MySecret"]);
This method worked without issue. To me it looks like they are doing the same thing. Changing a Base64 text value into an array of bytes. However, I must be missing something. Why does Convert.FromBase64String fail and TextEncodings.Base64Url.Decode work?
I came across the same thing when I migrated our authentication service to .NET Core. I had a look at the source code for the libraries we used in our previous implementation, and the difference is actually in the name itself.
The TextEncodings class has two types of text encoders, Base64TextEncoder and Base64UrlEncoder. The latter one modifies the string slightly so the base64 string can be used in an url.
My understanding is that it is quite common to replace + and / with - and _. As a matter of fact we have been doing the same with our handshake tokens. Additionally the padding character(s) at the end can also be removed. This leaves us with the following implementation (this is from the source code):
public class Base64UrlTextEncoder : ITextEncoder
{
public string Encode(byte[] data)
{
if (data == null)
{
throw new ArgumentNullException("data");
}
return Convert.ToBase64String(data).TrimEnd('=').Replace('+', '-').Replace('/', '_');
}
public byte[] Decode(string text)
{
if (text == null)
{
throw new ArgumentNullException("text");
}
return Convert.FromBase64String(Pad(text.Replace('-', '+').Replace('_', '/')));
}
private static string Pad(string text)
{
var padding = 3 - ((text.Length + 3) % 4);
if (padding == 0)
{
return text;
}
return text + new string('=', padding);
}
}

String array conversion does not work

I have written this sub to convert the a date in the format 20140101 to 01/01/2014. From the variable inDate I decided to load this into the array arrDate. Each element of the array is correct. It is when I try and put the array back into a string (outDate) I get the following result.
indate=20140101 outDate=97/01/2014
indate=20140202 outDate=98/02/2014
Can anyone say why?
Thanks,
Lawrence
private String convDate (String inDate) {
String outDate = "XXXXXXXXXX";
char[] arrDate = inDate.toCharArray();
switch (MainActivity.localloc){
case "E":
System.out.println("posO "+arrDate[0]);
System.out.println("pos1 "+arrDate[1]);
System.out.println("pos2 "+arrDate[2]);
System.out.println("pos3 "+arrDate[3]);
System.out.println("pos4 "+arrDate[4]);
System.out.println("pos5 "+arrDate[5]);
System.out.println("pos6 "+arrDate[6]);
System.out.println("pos7 "+arrDate[7]);
outDate=arrDate[6]+arrDate[7]+"/"+ arrDate[4]+arrDate[5]+"/"+arrDate[0]+arrDate[1]+arrDate[2]+arrDate[3];
}
return outDate;
}
private void Button2_Click(System.Object sender, System.EventArgs e)
{
string str = null;
string s = "20140101";
str = s.Substring(6, 2) + "/" + s.Substring(4, 2) + "/" + s.Substring(0, 4);
Interaction.MsgBox(str);
}
thanks for your reply. A much better way of solving my problem. I don't know what the difference is between Substring and substring is but the only way I could get it to work with substring was:
str=s.substring(6,8)+"/"+s.substring(4,6)+"/"+s.substring(0,4);
It seems substring wants the first and last character positions.
Lawrence

Array .indexOf returning -1 despite match

I'm writing a flexible search algorithm for a portion of my XML file. I have this exact same method working flawlessly elsewhere, thus why this is driving me batty.
Here is the section of my function that I'm having trouble with.
var searchResults:Array = [];
var r:Array = [];
//Other code...
var classSplit:Array = inClass.split("-");
var profs:Array = getCourseProfs(classSplit[0], classSplit[1]);
trace(searchResults + "-vs-" + profs);
for each(var si:String in searchResults)
{
trace(si + " is at index " + profs.indexOf(si));
trace(profs);
if(r.indexOf(si) == -1 && profs.indexOf(si) != -1)
{
r.push(si);
trace(r);
}
}
And here is the output for a particular run's trace statements (from the above code).
GABIOMA1,GABIOMA1-vs-GABIOMA1,MITCHKA1,GIBSOCA1
GABIOMA1 is at index -1
GABIOMA1,MITCHKA1,GIBSOCA1
GABIOMA1 is at index -1
GABIOMA1,MITCHKA1,GIBSOCA1
Final:
As you can see, the function has two results in the searchResults array (of strings). They're both identical since I haven't yet sorted out duplicate search results. "GABIOMA1,GABIOMA1"
Then, I'm comparing that to an array "profs" with these three values: "GABIOMA1,MITCHKA1,GIBSOCA1"
You can see the matching result in index 0 of profs. However, as you see from the next line, profs.indexOf(si)' whensi` is "GABIOMA1" incorrectly returns "-1".
I can write a workaround in a heartbeat, but I need to know why indexOf has decided to bork.
EDIT: Even more infurating, the code in the other branch of the IF statement from this works like a charm...
for each(var s:String in searchResults)
{
if(r.indexOf(s) == -1)
{
r.push(s);
}
}
EDIT 2: This is my WORKING workaround. As you can see, all of the arrays are working properly.
for each(var si:String in searchResults)
{
var match:Boolean = false;
for each(var pi:String in profs)
{
if(si == pi)
{
pr.push(si);
break;
}
}
}
Given your trace(profs) returns a single string, your array contains a string GABIOMA1,MITCHKA1,GIBSOCA1 as a single entity, that's why your index of GABIOMA1 returns -1. You need to do split by comma within getCourseProfs() to get your GABIOMA1 as a separate string.
UPDATE: I have tested with the following:
var t:Array = ['GABIOMA1', 'MISCHKA1', 'GIBSOCA1'];
trace(t);
trace(t.indexOf('GABIOMA1'));
var a:Array = ['MITCHKA1', 'GABIOMA1\x00'];
for each(var si:String in a) trace(si, 'is at index', t.indexOf(si));
This gave me an expected result of -1 for either element of a, and expected trace results as in your example, however, equality test fails. Please try trimming the searchResults strings prior to checking for indexOf().
A side note: placing a \x00 as the first char of a string produces different results for tracing! trace(si, 'is at index', t.indexOf(si)); results in an empty string displayed in place of si like this: is at index -1, while trace(si + ' is at index ' + t.indexOf(si)); results in full string displayed: GABIOMA1 is at index -1. So carefully check either string for non-printable symbols, it's likely there is some hidden zero char that screws indexOf().

Resources