Find all occurrences (letters) in array (word) - javascript

I want to create a hangman game in React.js, with this code when the user click on a letter, it will search the letter in the word and display her. It work correctly when the word contain only one same letter.
I would like this code work with word like 'PROGRAMMING' with 2 'M'.
handleChooseLetter = (index) => {
const usedWord = [...this.state.usedWord];
const chosenLetter = this.state.letters[index].letter;
var letterPosition = usedWord.indexOf(chosenLetter);
if (letterPosition >= 0) {
hiddenWord.splice(letterPosition, 1, chosenLetter);
this.setState({hiddenWord: hiddenWord});
}
}
I already try a while loop but it not work in my case:
var indices = [];
while(letterPosition >= 0) {
const hiddenWord = [...this.state.hiddenWord];
indices.push(letterPosition);
letterPosition = usedWord.indexOf(chosenLetter, letterPosition + 1);
hiddenWord.splice(letterPosition, 1, chosenLetter);
this.setState({hiddenWord: hiddenWord});
}
For me, the result is that find the letter and display them always for the last letter of the word.
I think my problem is with the splice method who splice the wrong letterPosition
Here my chooseWord function:
state = {
wordList: [
{ id: 1, word: 'PROGRAMMING'},
],
usedWord: [],
hiddenWord: [],
}
chooseWord() {
const wordList = [...this.state.wordList];
const listLength = wordList.length;
const randomWord = this.state.wordList[Math.floor(Math.random() * listLength)].word;
const splittedWord = randomWord.split("");
const arr = new Array(randomWord.length + 1).join("_").split("");
this.setState({
usedWord: splittedWord,
hiddenWord: arr
});
}

The simplest way is replace, not using an array:
const usedWord = "programming";
const chosenLetter = "m";
const hiddenWord = usedWord.replace(new RegExp("[^" + chosenLetter + "]", "g"), "_");
console.log(hiddenWord);
As the user adds more letters, you can add them to the character class:
const usedWord = "programming";
const chosenLetters = "mp";
const hiddenWord = usedWord.replace(new RegExp("[^" + chosenLetters + "]", "g"), "_");
console.log(hiddenWord);
React Example:
class Hangman extends React.Component {
constructor(...args) {
super(...args);
this.state = {
availableLetters: "abcdefghijklmnopqrstuvwxyz",
chosenLetters: "",
word: this.props.word
};
this.chooseLetter = this.chooseLetter.bind(this);
}
chooseLetter({target: {tagName, type, value}}) {
if (tagName === "INPUT" && type === "button") {
this.setState(prevState => ({chosenLetters: prevState.chosenLetters + value}));
}
}
render() {
const {word, chosenLetters} = this.state;
const hiddenWord = word.replace(new RegExp("[^" + chosenLetters + "]", "g"), "_");
return <div>
<div>Word: <span className="hiddenWord">{hiddenWord}</span></div>
<div onClick={this.chooseLetter} style={{marginTop: "8px"}}>
{[...this.state.availableLetters].map(
letter => <input type="button" value={letter} disabled={chosenLetters.includes(letter)} />
)}
</div>
</div>;
}
}
ReactDOM.render(
<Hangman word="programming" />,
document.getElementById("root")
);
.hiddenWord {
font-family: monospace;
letter-spacing: 1em;
font-size: 18px;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
For single English alphabet letters, you don't have to worry about using new RegExp(chosenLetter, "g") because none of the English alphabetic letters has special meaning in a regular expression. If you did have characters with special meaning (., $, etc.), you'd escape the character before passing it to the RegExp constructor; see this question's answers for ways to do that.

I've added letter input <input onChange={this.handleChooseLetter} value={letter} /> and changed your handleChooseLetter function to iterate through letters of used word if at least 1 letter is found (because your usedWord.indexOf(chosenLetter) always returns 1 index only), so I decided to iterate entire word and check for chosen letter, if letter on that index exists, I just insert that letter to the hidden word on the same index - because hidden and used words have the same length:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
hiddenWord: "___________",
usedWord: "PROGRAMMING",
letter: ""
};
}
handleChooseLetter = e => {
const usedWord = [...this.state.usedWord];
const chosenLetter = e.target.value.toLocaleUpperCase();
let letterPosition = usedWord.indexOf(chosenLetter);
if (letterPosition > -1) {
this.setState(prevState => {
const hiddenWord = [...prevState.hiddenWord];
for (let i = 0; i < usedWord.length; i++) {
if (usedWord[i] === chosenLetter) {
hiddenWord[i] = chosenLetter;
}
}
return { hiddenWord, letter: "" };
});
return;
}
this.setState({ letter: "" });
};
render() {
const { hiddenWord, letter } = this.state;
return (
<div className="App">
{[...hiddenWord].map((letter, index) => (
<span key={index}>{letter} </span>
))}
<input onChange={this.handleChooseLetter} value={letter} />
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.4.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.4.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Aha! I had to do a similar project for my class once. Heres the github repo for your reference:
https://github.com/LordKriegan/WraithFood
Essentially, what I did was I created an object with several strings and a few functions. When the game loads, it picks a random word from my list and sets 2 of the strings. One is the full word (lets call this property fullWord), the other is basically a string of the same length with all letters converted to underscores (let's call this one guessedWord, also see setWord() in the game.js file).
But you are interested in checking the letters! Thats easy enough. Javascript strings have a built-in method called .includes(), which takes a string as a parameter. Since I had my word saved in the object, I simply ran a .includes() on that word with the letter I wanted to check, then if it passed all my validation (letter already guessed, whether it is or isnt in the word, etc), I ran a for loop on guessedWord with another String method called .charAt(i). This method simply returns the character at position i in the string its called on. Since I have to strings in my object, which I KNOW to be the same length, I can execute a for loop on fullWord, check every letter at position i, and then reset the guessedWord property with .substr(). This method returns partial strings based on what parameters you pass it. Essentially I set guessedWord to + + . This was the checkLet() function in the game object.
I would reccomend reading up on string methods at https://www.w3schools.com/js/js_string_methods.asp

Related

How do I get dynamic index position for a specific character for a input text?

let's assume an input string has given, for a particular character like '$' you want to add dynamically in the text field , so I want to find the index of a current character like '$' then it's not working properly like initially if I give '$'in any position, its reflection the position, example if I give 'random text$' it returns index 11 but if you type '$'in between text like 'random $text$' then it should return 7, but it returns 12,so by achieving 7 I need to give extra space like 'random $ text$', so dynamically how to get the index position of a current character($), whether It's added in first, middle, last of the text
let string = "random $text$";
let newArray = string.split("");
let store = string.length % 2 !== 0
? newArray.findLastIndex((x) => x === "$")
: newArray.findIndex((x) => x === "$");
console.log(store);
As you just want to add a space after first occurance of $ in the input string. You can simply achieve that by using String.replace() method instead of looking for index as If pattern in replace function is a string, only the first occurrence will be replaced. The original string is left unchanged.
Live Demo :
function checkIndex(e) {
const val = e.target.value;
console.log(val.replace('$', '$ '));
}
<input type="text" id="val" onBlur="checkIndex(event)"/>
Update : Here is the workaround solution but not a good practice to do. For demo, I am using an input and after getting the index, I am replacing the $ with some other character so that I can get the index of current/newly added $.
Demo :
function checkIndex(e) {
const val = e.target.value;
if (/$/.test(val)) {
console.log(val.indexOf('$'))
document.getElementById('val').value = val.replaceAll('$', '-')
}
}
<input type="text" id="val" onBlur="checkIndex(event)"/>
I don't really understand why you check for first index/last based on if the total length is even/uneven. However if you want to get all indices you can use a reducer.
const string = "random $text$";
const charToFind = "$";
// Get all indexes of char charToFind in string
let indexes = [...string].reduce((previous, current, i) => {
// Checks if current value is equal to charToFind otherwise return previous
return current === charToFind ? [...previous, i] : previous;
}, []);
EDIT:
Per OPs new requirements in the comments this is what I could come up with. However its a bit overcomplicated, and will not always work.
// This only take to account of one change, it will not work with paste etc.
const findChar = (newState: string, oldState: string, lastKnownIndices: number[], charToFind: string) => {
// Find first diff between old and new state
const firstDiffIndex = newState
.split("")
.findIndex((char, index) => char !== oldState[index]);
// Check if we have added or removed a char
const isCharAdded = newState.length > oldState.length;
// Update last known indices,
let newIndices = lastKnownIndices.map((index) => {
if (index >= firstDiffIndex) {
return isCharAdded ? index + 1 : index - 1;
}
return index;
}).filter((index) => {
// Remove indices that are no longer valid
return newState[index] === charToFind;
});
// If we added a char, check if it is the char we are looking for
if (isCharAdded && newState[firstDiffIndex] === charToFind) {
newIndices = [firstDiffIndex, ...newIndices]
}
return {
newIndices,
newState,
lastIndex: newIndices.length > 0 ? newIndices[0] : -1
}
}
const noCharsToFind = findChar("random text", "random tex", [], "$")
console.log(noCharsToFind)
const addedOneCharToFind = findChar("random $text", noCharsToFind.newState, noCharsToFind.newIndices, "$")
console.log(addedOneCharToFind)
const addedOneMoreCharToFind = findChar("random $text$", addedOneCharToFind.newState, addedOneCharToFind.newIndices, "$")
console.log(addedOneMoreCharToFind)
const removedOneCharToFind = findChar("random text$", addedOneMoreCharToFind.newState, addedOneMoreCharToFind.newIndices, "$")
console.log(removedOneCharToFind)
Here is a link to playground if you want to test more cases

Bolding all words in string between certain characters

I am wanting to take a string and get the JSX to replace all the words in between brackets to be bold. I got it to work with this, but just wondering if there is a better way of going about this?
const jsxArray = [];
let unformattedString = "[name] Hi there, my name is [name], I am [age] years old, and I work in the field of [profession]";
const boldStrings = unformattedString.match(/(?<=\[).+?(?=\])/g);
const notBoldStrings = unformattedString.split(/\[.+?\]/g);
let j = 0;
if (boldStrings !== null) {
notBoldStrings.forEach((notBoldString, index) => {
if (index === 0 && notBoldStrings === "") {
// the first word should be bolded
jsxArray.push(<b>{boldStrings[j]}</b>);
} else {
jsxArray.push(notBoldString);
jsxArray.push(<b>{boldStrings[j]}</b>);
}
j++;
});
} else {
jsxArray.push(notBoldStrings[0]);
}
The expected output would be:
name Hi there, my name is name, I am age years old, and I work in the field of profession
You can use this code:
export default function App() {
let unformattedString =
"[name] Hi there, my name is [name], I am [age] years old, and I work in the field of [profession]";
const boldString = (str, substr) =>{
str.replaceAll(substr, `<b>${substr}</b>`);
}
const strArr = unformattedString.split(" ");
const formattedString = strArr
.map((item) => {
let strBold = item.match(/(?<=\[).+?(?=\])/g);
if (strBold) {
return boldString(item, strBold);
} else {
return item;
}
})
.join(" ");
return (
<div className="App">
<div dangerouslySetInnerHTML={{ __html: formattedString }} />
</div>
);
}
To see its result: codesandbox.io
But about dangerouslySetInnerHTML, Setting HTML from code is risky because it’s easy to inadvertently expose your users to a cross-site scripting (XSS) attack. And recommended using the html-react-parser package in your React project.
npm install html-react-parser
To use:
import parse from 'html-react-parser'
const yourHtmlString = '<h1>Hello</h1>'
and in return part:
<div>
{parse(yourHtmlString)}
</div>

How to type unique letters only in input text box in html or react?

I want to create an input box that allows to type only a distinct alphabet letter in the input box
( No duplicate alphabet value, ONLY ONE)
I looked up all the attributes for input box but I couldn't find one and there were no example.
Do I have to handle it within JavaScript functions?
(I am using React)
<input
className="App-Contain-Input"
name="containedLetter"
type={"text"}
onChange={containedChange}
/>
Here is how this can be done, note that we have to use onkeydown which fires when a key is being pressed, but not before it is being released (this way we can intercept and prevent the key stroke from being taken into account):
function MyInput() {
const containedChange = (event) => {
if (event.key.length === 1 && event.code.startsWith('Key') && event.code.length === 4 && event.target.value.indexOf(event.key) !== -1) {
event.preventDefault()
return false;
}
return true
}
return (
<input
id="input"
className="App-Contain-Input"
name="containedLetter"
type="text"
onkeydown={containedChange}
/>
}
A way to do it with a cheeky two-liner is to get rid of any non-letters with regex, then let Set do the de-duping by converting to Array => Set => Array => String:
As a side note, many of the other solutions posted here make one or both of two assumptions:
The user is only ever going to edit the last character... but what if they edit from the beginning or middle? Any last character solution will then fail.
The user will never copy/paste a complete value... again, if they do, most of these solutions will fail.
In general, it's best to be completely agnostic as to how the value is going to arrive to the input, and simply deal with the value after it has arrived.
import { useState } from 'react';
export default function Home() {
const [value, setValue] = useState('');
return (
<div>
<input
type='text'
value={value}
onChange={(e) => {
const onlyLetters = e.target.value.replace(/[^a-zA-Z]/g, '');
setValue(Array.from(new Set(onlyLetters.split(''))).join(''));
}}
/>
</div>
);
}
Your containedChange function can be like this:
const containedChange = (e) => {
const value = e.target.value;
const lastChar = value[value.length - 1];
if (value.slice(value.length - 1, value.length).indexOf(lastChar) != -1) {
// show error message
}
The function checks whether the last entered character already exists in the previous value or not.
An isogram is a word with no repeating letters. You can check this using regex
function isIsogram (str) {
return !/(.).*\1/.test(str);
}
Or using array.some
function isIsogram(str) {
var strArray = str.split("");
return !strArray.some(function(el,idx,arr){
return arr.lastIndexOf(el)!=idx;
});
}
In react - you need to use state to reject inputs which contain repeated letters (not an Isogram). Complete example on codesandbox.
import { useState } from 'react';
export default function App() {
const [ text, setText ] = useState('');
function isIsogram(str) {
var strArray = str.split("");
return !strArray.some(function(el,idx,arr){
return arr.lastIndexOf(el)!==idx;
});
}
function handleChange(e) {
const { value } = e.target;
console.log(isIsogram(value));
if (value.length === 1 || isIsogram(value)) {
setText(value);
}
}
return (
<input value={text} onChange={handleChange} />
);
}
Use a state to maintain the current value of the input. Then when the value changes get the last letter of the input, check that it's a letter, and if it is a letter and it isn't already in the state, update the state with the new input value.
const { useState } = React;
function Example() {
const [ input, setInput ] = useState('');
function handleChange(e) {
const { value } = e.target;
const lastChar = value.slice(-1);
const isLetter = /[a-zA-Z]/.test(lastChar);
if (isLetter && !input.includes(lastChar)) {
setInput(value);
}
}
function handleKeyDown(e) {
if (e.code === 'Backspace') {
setInput(input.slice(0, input.length - 1));
}
}
return (
<input
value={input}
onChange={handleChange}
onKeyDown={handleKeyDown}
/>
);
}
ReactDOM.render(
<Example />,
document.getElementById('react')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>
One pure js solution would be to handle the input value in onkeyup event where we have access to the latest key as well as updated input target value.
If the latest key pressed is duplicate then we will found it on 2 location in the target value, first on some where middle of the target value and second at the end of the target value. So we can remove the value at the end (which is the duplicate one).
The below code snippet shows the implementation of above in React
const Input = () => {
const handleKeyUp = (e) => {
const { key, target: { value }} = e;
const len = value.length;
if (value.indexOf(key) < len - 2) {
e.target.value = value.slice(0, len - 1);
}
}
return (
<div>
<label>Unique Keywords</label>
<input type="text" placeholder="my-input" onKeyUp={handleKeyUp} />
</div>
);
}

How to replace text with substitute that contains JSX code?

interface ICard {
content: string,
blanks: Array<{word: string, hidden: boolean}>
}
function processCards():Array<any>{
if (cards !==null ){
const text = cards.map((card,cardIndex)=>{
var content = card.content
card.blanks.map((blank,blankIndex)=>{
// replace content
const visibility = (blank.hidden)?'hidden':'visible'
const click_blank = <span className={visibility} onClick={()=>toggleBlank(cardIndex,blankIndex)}>{blank.word}</span>
content = content.replace(blank.word,click_blank)
})
return content
})
return text
} else {
return []
}
}
I have an array of objects of type ICard.
Whenever card.blanks.word appears in card.content, I want to wrap that word in tags that contain a className style AND an onClick parameter.
It seems like I can't just replace the string using content.replace like I've tried, as replace() does not like the fact I have JSX in the code.
Is there another way to approach this problem?
You need to construct a new ReactElement from the parts of string preceding and following each blank.word, with the new span stuck in the middle. You can do this by iteratively building an array and then returning it wrapped in <> (<React.Fragment>). Here's a (javascript) example:
export default function App() {
const toggleBlankPlaceholder = (cardIndex, blankIndex) => {};
const cardIndexPlaceholder = 0;
const blanks = [
{ word: "foo", hidden: true },
{ word: "bar", hidden: false },
];
const content = "hello foo from bar!";
const res = [content];
for (const [blankIndex, { word, hidden }] of blanks.entries()) {
const re = new RegExp(`(.*?)${word}(.*)`);
const match = res[res.length - 1].match(re);
if (match) {
const [, prefix, suffix] = match;
res[res.length - 1] = prefix;
const visibility = hidden ? "hidden" : "visible";
res.push(
<span
className={visibility}
onClick={() =>
toggleBlankPlaceholder(cardIndexPlaceholder, blankIndex)
}
>
{word}
</span>
);
res.push(suffix);
}
}
return <>{res}</>;
}
The returned value will be hello <span class="hidden">foo</span> from <span class="visible">bar</span>!
A couple of things:
In your example, you used map over card.blanks without consuming the value. Please don't do that! If you don't intend to use the new array that map creates, use forEach instead.
In my example, I assumed for simplicity that each entry in blanks occurs 0 or 1 times in order in content. Your usage of replace in your example code would only have replaced the first occurrence of blank.word (see the docs), though I'm not sure that's what you intended. Your code did not make an ordering assumption, so you'll need to rework my example code a little depending on the desired behavior.

Check string for array elements and systematically replace them with HTML tags - (JS, ReactJS)

I need to search a string, and if it has any values that match my array, I need to add <span></span> tags to them to add custom CSS. I am using reactJS.
How do I search the string for objects from my array?
Example:
let string = 'this is a message with many inputs, {{input1}}, {{input2}}, and again {{input1}}'
let array = [{parameter: '{{input1}}'},{parameter: '{{input2}}'},...]
findAllOccurrances = () => {???}
Then systematically replace them '{{inputX}}' with <span className='bizarre-highlight'>{{inputX}}</span>
My intent is to add custom CSS to any text in the div which matches my array, so if you got any ideas please shoot! Again, using reactJS if that helps.
I created a component that will replace the elements that need to be highlighted with a span you can test it here
The component is:
import React from 'react';
export default ({ terms, children }) => {
const result = []
const regex = terms.map(escapeRegExp).join('|');
const re = new RegExp(regex);
let text = (' ' + children).slice(1); // copy
let match = re.exec(text);
while (match != null) {
const str = match.toString();
result.push(text.slice(0, match.index));
result.push(<span className="highlighted">{str}</span>);
text = text.slice(match.index + str.length);
match = re.exec(text);
}
result.push(text);
return result;
}
function escapeRegExp (str) {
return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&");
}
And you should use it like this:
import React from 'react';
import Highlighter from './Highlighter';
const terms = [ '{{input1}}', '{{input2}}' ]
const App = () => (
<div>
<Highlighter terms={terms}>
{'this is a message with many inputs, {{input1}}, {{input2}}, and again {{input1}}'}
</Highlighter>
</div>
);
Use String#replace with a RegExp to find all instances of '{{inputX}}', and wrap the matches with the span:
const string = 'this is a message with many inputs, {{input1}}, {{input2}}, and again {{input3}}'
const array = [{parameter: '{{input1}}'},{parameter: '{{input2}}'}]
const pattern = new RegExp(array.map(({ parameter }) => parameter).join('|'), 'g');
const result = string.replace(pattern, (match) =>
`<span className='bizarre-highlight'>${match}</span>`
)
console.log(result)
use Array#map to extract values for wrapping in <span> and then cycle on them for replacement:
let string = 'this is a message with many inputs, {{input1}}, {{input2}}, and again {{input1}}';
let array = [{parameter: '{{input1}}'},{parameter: '{{input2}}'}];
array.map(el => { return el.parameter }).forEach(str => {
string = string.split(str).join("<span className=\'bizarre-highlight\'>" + str + "</span>");
});
console.log(string);

Categories