I'm working on a CLI program, based on nodejs and the npm package "prompt".
Let say I want to have this prompt, putting the input in a variable pet:
Choose a pet:
(1) - Cat
(2) - Dog
(3) - Fish
(4) - Rabbit
(5) - Rat
: >
Basically I did the functionality, but I'm having the following problems:
If I use the conform function for custom validation - then my custom message - the multiline text - never appears. The name of the variable - pet - only appears. But I want to have validation, cause I want to make sure the user won't enter 333 for example.
If I remove the conform custom validation - I can have multiline text, but then something else happens: the blinking rectangle, where the entering happens, overlaps with the multiline text. And I can't make it blink after the last line of the multiline message.
(In the above example the blinking happens over the digit 5.)
Any idea how to resolve the two issues I have ? ... :)
================== EDIT: Added code samples ===================
This is how I generate the multiline text:
// generate the multiline text ..
var petsMessage = 'Choose a pet: \n';
var pets = [...];
for(var i = 0, l = pets.length; i < l; i++) {
petsMessage += ' (' + (i+1) + ') - ' + pets[i] + "\n";
}
This is how I generate the prompt with multiline text, but no validation:
// define the prompt stuff ..
var promptInfo = {
properties: {
Pet: {
message: petsMessage,
required: true
},
}
};
And this is with validation, but multiline message not working:
// define the prompt stuff ..
var promptInfo = [
{
name: 'Pet',
message: petsMessage,
required: true,
conform: function(value) {
value = parseInt(value);
if(value > 0 && value < pets.length) {
return true;
} else {
return false;
}
}
}
];
I believe the problem was that in the second snippet with the validation you assign the actual question in the message property, you should assign it in the description. The message property refers to error message. Try this please:
var petsMessage = 'Choose a pet: \n';
var pets = ["dog","cat","frog"];
for(var i = 0, l = pets.length; i < l; i++) {
petsMessage += '\t (' + (i+1) + ') - ' + pets[i] + "\n";
}
var prompt = require('prompt');
var promptInfo = [
{
name: 'Pet',
description: petsMessage,
required: true,
message: 'Options allowed:: 1'+'-'+pets.length,
conform: function(value) {
value = parseInt(value);
return value > 0 && value <= pets.length
}
}
];
prompt.start();
prompt.get(promptInfo, function (err, result) {
console.log('you Choose',result,'::::',pets[result.Pet-1])
});
Actually, the solution from "alex-rokabills" is not perfect too :( ... It's definitely better, but I still see issues.
If I use small amount of items then it's OK:
But if the number grows a little bit:
And for big prompts:
Also - can I get rid of the "prompt:" at the begining ? ...
Related
i work on a comunautary bot for discord and i have a probleme in my code
this is my code :
for (let i = 0; i < body.players.length; i++) {
if ((body.players[i].votes >= myToken[0]['palier1'] ) && (body.players[i].votes < myToken[0]['palier2'] )) {
embed50.addFields(
{
name: body.players[i].playername + ' : ',
value: body.players[i].votes + ' votes ! ',
inline: true,
})
}else if((body.players[i].votes >= myToken[0]['palier2'] )) {
embed100.addFields(
{
name: body.players[i].playername + ' : ',
value: body.players[i].votes + ' votes ! ',
inline: true,
})
}
}
and this is my error :
return comparator(input, number) ? Result.ok(input) : Result.err(new ExpectedConstraintError(name, "Invalid number value", input, expected));
^
ExpectedConstraintError: Invalid number value
at Object.run (/root/bot/node_modules/#sapphire/shapeshift/dist/index.js:727:72)
at /root/bot/node_modules/#sapphire/shapeshift/dist/index.js:113:66
at Array.reduce (<anonymous>)
at NumberValidator.parse (/root/bot/node_modules/#sapphire/shapeshift/dist/index.js:113:29)
at validateFieldLength (/root/bot/node_modules/#discordjs/builders/dist/index.js:146:24)
at EmbedBuilder.addFields (/root/bot/node_modules/#discordjs/builders/dist/index.js:190:5)
at Object.run (/root/bot/commands/topvote.js:111:18)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
constraint: 's.number.lessThanOrEqual',
given: 26,
expected: 'expected <= 25'
}
my first if is okay but the second block my code .....
The stacktrace you posted suggests that the error is coming from a function in the Discord.js library called validateFieldLength.
Looking at the source code for this function in Discord.js it appears the name of this function might be a bit confusing; at first, I thought it was saying the name or value of a field was too long, but it appears that it's actually saying that you're adding too many fields to a single embed, you've added 26 and you're only allowed to add <= 25.
Here is a link to the documentation that goes over the limits for embeds: https://discordjs.guide/popular-topics/embeds.html#embed-limits
I would suggest you track how many fields you've added so far and when you've hit 25 go ahead and create a new embed (keep an array of these) and attach the next batch to that one. Just note that you can only have up to 10 embeds in a single message so you'll have to stop at 250.
Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 6 years ago.
Improve this question
This is a new edit to my question, hopefully it will meet the criteria and be considered eligible.
First, I managed to solve the problem. I will now describe the situation and what I think the solution that solve the problem.
My code gets a string (a call number) as an input, re-formats it, parse it to float, and return the call number location within a given set of ranges.
The code is composed of two functions: 1. formatCallNumber(callNum) which does the text manipulation to the input. 2. SortCallNum(callNumInput) - responsible on the sorting to ranges part.
The problem was in passing values of call number ranges from the sorting function (no.2) to the formatting function (no.1). Although I parsed those values as strings in the sorting function, the .replace function produced an error. The solution that I (think) worked, was to parse the values to strings in the formatting function.
The code of the two functions below is updated and seems to be working as expected:
function 1 - formatting function:
function formatCallNumber(callNum){
var formatedCallNum = String(callNum);
formatedCallNum = formatedCallNum.replace(/\D/g,''); // remove all but digits chars from the string (whitespace, dots, etc)
formatedCallNum = "0." + formatedCallNum; // add "0." to the callNumber string
formatedCallNum = parseFloat(formatedCallNum); // parse as float - so it could be compared with other decimals
return (formatedCallNum);
}
Function 2 - the sorting function:
function SortCallNum(callNumInput){
// data [test only]
var shelves = {
"S1" : {"callStart":"100","callEnd": "223.456", "id": 1},
"S2" : {"callStart":"223.457","callEnd": "334", "id": 2},
"S3" : {"callStart":"335","callEnd": "535", "id": 3},
"S4" : {"callStart":"536","callEnd": "638", "id": 4},
"S5" : {"callStart":"639","callEnd": "847", "id": 5}
};
var matchId = "";
document.getElementById("somthing").innerHTML += "you typed the number: " + callNumInput; // output of callNumInput (as inserted by user)
formatedCallNum = formatCallNumber(callNumInput);
// traverse into shelves object : iteration of objects (key = s1-s5)
for (var key in shelves) {
if (shelves.hasOwnProperty(key)) {
matchId = shelves[key].id;
document.getElementById("somthing").innerHTML += "<br>" + (" -- " + "CallEnd is: " + " -- " + shelves[key].callEnd); // display values of object shelves.key.callend
document.getElementById("somthing").innerHTML += "<br>" + (" -- " + "CallStart is: " + " -- " + shelves[key].callStart); // display values of object shelves.key.callend
var formatedCallRangeStart = formatCallNumber(shelves[key].callStart);
var formatedCallRangeEnd = formatCallNumber(shelves[key].callEnd);
console.log(formatedCallRangeStart);
console.log(formatedCallRangeEnd);
if ((formatedCallNum <= 0) || (formatedCallNum > 1)){alert('call number not in proper range'); break;}
if ((formatedCallRangeStart <= formatedCallNum)&&(formatedCallRangeEnd >= formatedCallNum)){break;}
}
}
Thanks for all the help.
As I can see, everythign should work as expected. It's important to pass a string into SortCallNum, and not a number.
function SortCallNum(callNumInput){
// data [test only]
var shelves = {
"S1" : {"callStart":100,"callEnd": "223", "id": 1},
"S2" : {"callStart":224,"callEnd": "334", "id": 2},
"S3" : {"callStart":335,"callEnd": "535", "id": 3},
"S4" : {"callStart":536,"callEnd": "638", "id": 4},
"S5" : {"callStart":639,"callEnd": "847", "id": 5}
};
var matchId = "";
document.getElementById("somthing").innerHTML += "you typed the number: " + callNumInput; // output of callNumInput (as inserted by user)
formatedCallNum = formatCallNumber(callNumInput);
// traverse into shelves object : iteration of objects (key = s1-s5)
for (var key in shelves) {
if (shelves.hasOwnProperty(key)) {
matchId = shelves[key].id;
document.getElementById("somthing").innerHTML += "<br>" + (" -- " + "CallEnd is: " + " -- " + shelves[key].callEnd); // display values of object shelves.key.callend
document.getElementById("somthing").innerHTML += "<br>" + (" -- " + "CallStart is: " + " -- " + shelves[key].callStart); // display values of object shelves.key.callend
var formatedCallRangeStart = String(shelves[key].callStart);
formatedCallRangeStart = formatCallNumber(formatedCallRangeStart);
var formatedCallRangeEnd = String(shelves[key].callEnd);
formatedCallRangeEnd = formatCallNumber(formatedCallRangeEnd);
matchId = shelves[key].id;
if ((formatedCallRangeStart <= formatedCallNum)&&(formatedCallRangeEnd >= formatedCallNum)){
break;
}
}
}
alert (matchId);
}
function formatCallNumber(callNum){
// callNum = prompt('enter a call number: ');
formattedCallNum = callNum.replace(/\D/g,''); // remove all but digits chars from the string (whitespace, dots, etc)
formattedCallNum = "0." + formattedCallNum; // add "0." to the callNumber string
formattedCallNum = parseFloat(formattedCallNum); // parse as float - so it could be compared with other decimals
return (formattedCallNum);
}
SortCallNum('1337')
<div id="somthing"></div>
So, this would work SortCallNum('1337'), this not SortCallNum(1337)...
Another possible cause is that you trust the return-value from prompt blindly.
<button type="button" onclick="var callNumInput = prompt('enter a call number: '); SortCallNum(callNumInput);"> SortCallNum(test)</button>
When the user clicks the OK button, text entered in the input field is returned. If the user clicks OK without entering any text, an empty string is returned. If the user clicks the Cancel button, this function returns null.
https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt#Example
A bit of sanitization should help:
if (callNumInput == null) {
throw new Error('You have to insert a number between 0 and 999.')
}
Situation - looping over array of events and assigning properties from JSON parsed
Expected outcome - upload to Parse cloud storage
APIs that I'm using -
https://www.eventbrite.com/developer/v3/formats/event/#ebapi-std:format-event
https://www.parse.com/docs/js/guide
I'm new to Javascript (there actually might be more than one syntax error)
I don't know why I get this error on line 83 when trying to deploy to Parse Cloud Code
What I'm passing in -
var cities = ["San Francisco", "London"];
eventsArray = JSON.parse(httpResponse.text)["events"];
loopEvents(eventsArray);
the whole function as screenshot (syntax highlighting for readability) --> code
the function as text -->
function loopEvents(events) {
if (j == cities.length) {j=0};
for (var i = 0; i < events.length; i++) {
//Parse.Cloud.useMasterKey(); is not needed ATM I think
console.log("assigning properties for " + cities[j] + ".");
list.save({ // saving properties
number: String(i); // ****THIS IS THE LINE 83****
uri: events[i]["resource_uri"];
url: events[i]["url"];
id: events[i]["id"];
name: events[i]["name"]["text"];
description: events[i]["description"]["text"] || "None provided.";
status: events[i]["status"];
capacity: String(events[i]["capacity"]);
logo: events[i]["logo_id"]["logo"] || "http://www.ecolabelindex.com/files/ecolabel-logos-sized/no-logo-provided.png";
start: moment(events[i]["start"]["utc"]);
end: moment(events[i]["end"]["utc"]);
online: events[i]["online_event"];
currency: events[i]["currency"];
ticketClasses: events[i]["ticket_classes"] || "It's freeee!";
ticketClassesNames: events[i]["ticket_classes"]["name"] || "None provided.";
ticketClassesCost: events[i]["ticket_classes"]["cost"] || "It's freeee!";
ticketClassesDescription: events[i]["ticket_classes"]["description"] || "None provided.";
}, {
success: function(list) {
console.log("RIP CloudCode, we had good times!");
},
error: function(list, error) {
console.log("u fuc*ed up, with error: " + error.text + ", son.");
}
});
}
j++;
}
maybe it's all wrong, appreciate the effort and constructive answers ;))) if you need any other info just comment bellow and I'll edit.
EDIT.1 - after replacing ; for , I get the following error
As you're using object, semi-colon ; is not valid syntax.
Remove ; from all the lines inside the object.
number: String(i);
// ^
Use , comma instead.
number: String(i),
// ^
Code
// Notice the comma at the end of each element
list.save({ // saving properties
number: String(i),
uri: events[i]["resource_uri"],
url: events[i]["url"],
id: events[i]["id"],
name: events[i]["name"]["text"],
description: events[i]["description"]["text"] || "None provided.",
status: events[i]["status"],
capacity: String(events[i]["capacity"]),
logo: events[i]["logo_id"]["logo"] || "http://www.ecolabelindex.com/files/ecolabel-logos-sized/no-logo-provided.png",
start: moment(events[i]["start"]["utc"]),
end: moment(events[i]["end"]["utc"]),
online: events[i]["online_event"],
currency: events[i]["currency"],
ticketClasses: events[i]["ticket_classes"] || "It's freeee!",
ticketClassesNames: events[i]["ticket_classes"]["name"] || "None provided.",
ticketClassesCost: events[i]["ticket_classes"]["cost"] || "It's freeee!",
ticketClassesDescription: events[i]["ticket_classes"]["description"] || "None provided."
}, {
See Object creation
ticket classes is actually an array and to access it I had to add a expand parameter to the httpRequest, other than that the code itself was fine, thx Tushar for the syntax correction.
it's my first question here. I tried to find an answer but couldn't, honestly, figure out which terms should I use, so sorry if it has been asked before.
Here it goes:
I have thousands of records in a .txt file, in this format:
(1, 3, 2, 1, 'John (Finances)'),
(2, 7, 2, 1, 'Mary Jane'),
(3, 7, 3, 2, 'Gerald (Janitor), Broflowski'),
... and so on. The first value is the PK, the other 3 are Foreign Keys, the 5th is a string.
I need to parse them as JSON (or something) in Javascript, but I'm having troubles because some strings have parentheses+comma (on 3rd record, "Janitor", e.g.), so I can't use substring... maybe trimming the right part, but I was wondering if there is some smarter way to parse it.
Any help would be really appreciated.
Thanks!
You can't (read probably shouldn't) use a regular expression for this. What if the parentheses contain another pair or one is mismatched?
The good news is that you can easily construct a tokenizer/parser for this.
The idea is to keep track of your current state and act accordingly.
Here is a sketch for a parser I've just written here, the point is to show you the general idea. Let me know if you have any conceptual questions about it.
It works demo here but I beg you not to use it in production before understanding and patching it.
How it works
So, how do we build a parser:
var State = { // remember which state the parser is at.
BeforeRecord:0, // at the (
DuringInts:1, // at one of the integers
DuringString:2, // reading the name string
AfterRecord:3 // after the )
};
We'll need to keep track of the output, and the current working object since we'll parse these one at a time.
var records = []; // to contain the results
var state = State.BeforeRecord;
Now, we iterate the string, keep progressing in it and read the next character
for(var i = 0;i < input.length; i++){
if(state === State.BeforeRecord){
// handle logic when in (
}
...
if(state === State.AfterRecord){
// handle that state
}
}
Now, all that's left is to consume it into the object at each state:
If it's at ( we start parsing and skip any whitespaces
Read all the integers and ditch the ,
After four integers, read the string from ' to the next ' reaching the end of it
After the string, read until the ) , store the object, and start the cycle again.
The implementation is not very difficult too.
The parser
var State = { // keep track of the state
BeforeRecord:0,
DuringInts:1,
DuringString:2,
AfterRecord:3
};
var records = []; // to contain the results
var state = State.BeforeRecord;
var input = " (1, 3, 2, 1, 'John (Finances)'), (2, 7, 2, 1, 'Mary Jane'), (3, 7, 3, 2, 'Gerald (Janitor), Broflowski')," // sample input
var workingRecord = {}; // what we're reading into.
for(var i = 0;i < input.length; i++){
var token = input[i]; // read the current input
if(state === State.BeforeRecord){ // before reading a record
if(token === ' ') continue; // ignore whitespaces between records
if(token === '('){ state = State.DuringInts; continue; }
throw new Error("Expected ( before new record");
}
if(state === State.DuringInts){
if(token === ' ') continue; // ignore whitespace
for(var j = 0; j < 4; j++){
if(token === ' ') {token = input[++i]; j--; continue;} // ignore whitespace
var curNum = '';
while(token != ","){
if(!/[0-9]/.test(token)) throw new Error("Expected number, got " + token);
curNum += token;
token = input[++i]; // get the next token
}
workingRecord[j] = Number(curNum); // set the data on the record
token = input[++i]; // remove the comma
}
state = State.DuringString;
continue; // progress the loop
}
if(state === State.DuringString){
if(token === ' ') continue; // skip whitespace
if(token === "'"){
var str = "";
token = input[++i];
var lenGuard = 1000;
while(token !== "'"){
str+=token;
if(lenGuard-- === 0) throw new Error("Error, string length bounded by 1000");
token = input[++i];
}
workingRecord.str = str;
token = input[++i]; // remove )
state = State.AfterRecord;
continue;
}
}
if(state === State.AfterRecord){
if(token === ' ') continue; // ignore whitespace
if(token === ',') { // got the "," between records
state = State.BeforeRecord;
records.push(workingRecord);
workingRecord = {}; // new record;
continue;
}
throw new Error("Invalid token found " + token);
}
}
console.log(records); // logs [Object, Object, Object]
// each object has four numbers and a string, for example
// records[0][0] is 1, records[0][1] is 3 and so on,
// records[0].str is "John (Finances)"
I echo Ben's sentiments about regular expressions usually being bad for this, and I completely agree with him that tokenizers are the best tool here.
However, given a few caveats, you can use a regular expression here. This is because any ambiguities in your (, ), , and ' can be attributed (AFAIK) to your final column; as all of the other columns will always be integers.
So, given:
The input is perfectly formed (with no unexpected (, ), , or ').
Each record is on a new line, per your edit
The only new lines in your input will be to break to the next record
... the following should work (Note "new lines" here are \n. If they're \r\n, change them accordingly):
var input = /* Your input */;
var output = input.split(/\n/g).map(function (cols) {
cols = cols.match(/^\((\d+), (\d+), (\d+), (\d+), '(.*)'\)/).slice(1);
return cols.slice(0, 4).map(Number).concat(cols[4]);
});
The code splits on new lines, then goes through row by row and splits into cells using a regular expression, which greedily attributes as much as it can to the final cell. It then turns the first 4 elements into integers, and sticks the 5th element (the string) onto the end.
This gives you an array of records, where each record is itself an array. The first 4 elements are your PK's (as integers) and your 5th element is the string.
For example, given your input, use output[0][4] to get "Gerald (Janitor), Broflowski", and output[1][0] to get the first PK 2 for the second record (don't forget JavaScript arrays are zero-indexed).
You can see it working here: http://jsfiddle.net/56ThR/
Another option would be to convert it into something that looks like an Array and eval it. I know it is not recommended to use eval, but it's a cool solution :)
var lines = input.split("\n");
var output = [];
for(var v in lines){
// Remove opening (
lines[v] = lines[v].slice(1);
// Remove closing ) and what is after
lines[v] = lines[v].slice(0, lines[v].lastIndexOf(')'));
output[v] = eval("[" + lines[v] + "]");
}
So, the eval parameter would look like: [1, 3, 2, 1, 'John (Finances)'], which is indeed an Array.
Demo: http://jsfiddle.net/56ThR/3/
And, it can also be written shorter like this:
var lines = input.split("\n");
var output = lines.map( function(el) {
return eval("[" + el.slice(1).slice(0, el.lastIndexOf(')') - 1) + "]");
});
Demo: http://jsfiddle.net/56ThR/4/
You can always do it "manually" :)
var lines = input.split("\n");
var output = [];
for(var v in lines){
output[v] = [];
// Remove opening (
lines[v] = lines[v].slice(1);
// Get integers
for(var i = 0; i < 4; ++i){
var pos = lines[v].indexOf(',');
output[v][i] = parseInt(lines[v].slice(0, pos));
lines[v] = lines[v].slice(pos+1);
}
// Get string betwen apostrophes
lines[v] = lines[v].slice(lines[v].indexOf("'") + 1);
output[v][4] = lines[v].slice(0, lines[v].indexOf("'"));
}
Demo: http://jsfiddle.net/56ThR/2/
What you have here is basically a csv (comma separated value) file which you wish to parse.
The easiest way would be to use an wxternal library that will take care of most of the issues you have
Example: jquery csv library is a good one. https://code.google.com/p/jquery-csv/
as the title says I need to extract content out of long text with certain fields.
I have this text as below
Name: David Jones
Office Address: 148 Hulala Street Date: 24/11/2013
Agent No: 1234,
Address: 259 Yolo Road Start Date: 22/11/2013 Due Date: 29/11/2013
Type: Human Properties: None Ago: 29
And I have these labels for specific fields in the text
Name, Office Address, Date, Agent No, Address, Type, Properties, Age
And the result I want to get is
Name: 'David Jones',
Office Address: '148 Hulala Street',
Date: '24/11/2013',
Agent No: '1234',
Address: '259 Yolo Road',
Type: 'Human'
Properties: 'None',
Age: ''
that has completely parsed the content with each field. Important thing to note here is the original text can possibly have typo (E.g., Ago instead of Age) and extra fields that do not exist in the list of labels (E.g., Start Date and Due Date do not exist in the label list). So the code will ignore any un-matching text and try to find only matching result.
I tried to resolve this by going through loops for each line, check if a line contains the field, and see if the line also contains more fields.
Currently I have the following code.
structure = ['Name','Office Address','Date','Agent No','Address','Type','Properties','Age'];
obj = {};
for (i = 0; i < textLines.length; i++) {
matchingFields = [];
for (j = 0; j < structure.length; j++) {
if (textLines[i].indexOf(structure[j] + ':') !== -1) {
if (matchingFields.length === 0 && textLines[i].indexOf(structure[j] + ':') === 0) {
matchingFields.push(structure[j]);
structure.splice(structure.indexOf(structure[j--]), 1);
} else if (textLines[i].indexOf(structure[j] + ':') > textLines[i].indexOf(matchingFields[matchingFields.length-1])) {
matchingFields.push(structure[j]);
structure.splice(structure.indexOf(structure[j--]), 1);
}
}
for (j = 0; j < matchingFields.length; j++) {
if (j !== matchingFields.length-1) {
obj[matchingFields[j]] = textLines[i].slice(textLines[i].indexOf(matchingFields[j]) + matchingFields[j].length, textLines[i].indexOf(matchingFields[j+1]));
} else {
obj[matchingFields[j]] = textLines[i].slice(textLines[i].indexOf(matchingFields[j]) + matchingFields[j].length);
}
obj[matchingFields[j]] = obj[matchingFields[j]].replace(':', '');
if (obj[matchingFields[j]].indexOf(' ') === 0) {
obj[matchingFields[j]] = obj[matchingFields[j]].replace(' ', '');
}
if (obj[matchingFields[j]].charAt(obj[matchingFields[j]].length-1) === ' ') {
obj[matchingFields[j]] = obj[matchingFields[j]].slice(0, obj[matchingFields[j]].length-1);
}
}
}
In some cases it could work fine but with 'Office Address: ' and 'Address: ' existing value for 'Office Address:' goes into 'Address:'. Besides, the code looks messy and ugly. Also seems like kind of brute forcing.
I guess there should be a better way. For example using regular expression or something similar. but no external library.
If you have any idea I will appreciate it for sharing.
Assuming the properties are separated by newline characters, you create an object mapping each attribute to its value using:
var str = "Name: David Jones\nOffice Address: 148 Hulala Street\nDate: 24/11/2013\nAgent No: 1234,\nAddress: 259 Yolo Road\\nType: Human Properties: None Age: 29";
var output = {};
str.split(/\n/).forEach(function(item){
var match = (item.match(/([A-Za-z\s]*):\s([A-Za-z0-9\s\/]*)/));
output[match[1]] = match[2];
});
console.log(output)
This may help:
> a.substr(a.indexOf("Name"), a.indexOf("Office Address")).split(":")
["Name", " David Jones "]