| 1 | within ;
|
|---|
| 2 | package KeyWordIO "Read and write data files with key words"
|
|---|
| 3 | function writeRealVariable "Writing real variable to file"
|
|---|
| 4 | input String fileName "Name of file" annotation(Dialog(__Dymola_loadSelector(filter = "Text files (*.txt; *.dat)", caption = "Open file in which Real parameters are present")));
|
|---|
| 5 | input String name "Name of parameter";
|
|---|
| 6 | input Real data "Actual value of parameter";
|
|---|
| 7 | input Boolean append = false "Append data to file";
|
|---|
| 8 | algorithm
|
|---|
| 9 | if not append then
|
|---|
| 10 | Modelica.Utilities.Files.removeFile(fileName);
|
|---|
| 11 | end if;
|
|---|
| 12 | Modelica.Utilities.Streams.print(name + " = " + String(data), fileName);
|
|---|
| 13 | end writeRealVariable;
|
|---|
| 14 |
|
|---|
| 15 | function writeRealVariables "Write multiple real variables to file"
|
|---|
| 16 | input String fileName "Name of file" annotation(Dialog(__Dymola_loadSelector(filter = "Text files (*.txt; *.dat)", caption = "Open file in which Real parameters are present")));
|
|---|
| 17 | input String name[:] "Name of parameter";
|
|---|
| 18 | input Real data[:] "Actual value of parameter";
|
|---|
| 19 | input Boolean append = false "Append data to file";
|
|---|
| 20 | algorithm
|
|---|
| 21 | // Check sizes of name and data
|
|---|
| 22 | if size(name, 1) <> size(data, 1) then
|
|---|
| 23 | assert(false, "writeReadParameters: Lengths of name and data have to be equal");
|
|---|
| 24 | end if;
|
|---|
| 25 | // Write data to file
|
|---|
| 26 | if not append then
|
|---|
| 27 | Modelica.Utilities.Files.removeFile(fileName);
|
|---|
| 28 | end if;
|
|---|
| 29 | for k in 1:size(name, 1) loop
|
|---|
| 30 | Modelica.Utilities.Streams.print(name[k] + " = " + String(data[k]), fileName);
|
|---|
| 31 | end for;
|
|---|
| 32 | end writeRealVariables;
|
|---|
| 33 |
|
|---|
| 34 | function readRealParameter "Read the value of a Real parameter from file"
|
|---|
| 35 | import Modelica.Utilities.*;
|
|---|
| 36 | input String fileName "Name of file" annotation(Dialog(__Dymola_loadSelector(filter = "Text files (*.txt; *.dat)", caption = "Open file in which Real parameters are present")));
|
|---|
| 37 | input String name "Name of parameter";
|
|---|
| 38 | input Boolean cache = false "Read file with/without caching";
|
|---|
| 39 | output Real result "Actual value of parameter on file";
|
|---|
| 40 | protected
|
|---|
| 41 | String line;
|
|---|
| 42 | String identifier;
|
|---|
| 43 | String delimiter;
|
|---|
| 44 | Integer nextIndex;
|
|---|
| 45 | Integer iline = 1;
|
|---|
| 46 | Types.TokenValue token;
|
|---|
| 47 | String message = "in file \"" + fileName + "\" on line ";
|
|---|
| 48 | String message2;
|
|---|
| 49 | Boolean found = false;
|
|---|
| 50 | Boolean endOfFile = false;
|
|---|
| 51 | algorithm
|
|---|
| 52 | if not cache then
|
|---|
| 53 | (line, endOfFile) := readLineWithoutCache(fileName, iline);
|
|---|
| 54 | else
|
|---|
| 55 | (line, endOfFile) := Streams.readLine(fileName, iline);
|
|---|
| 56 | end if;
|
|---|
| 57 | while not found and not endOfFile loop
|
|---|
| 58 | (token, nextIndex) := Strings.scanToken(line);
|
|---|
| 59 | if token.tokenType == Types.TokenType.NoToken then
|
|---|
| 60 | iline := iline + 1;
|
|---|
| 61 | elseif token.tokenType == Types.TokenType.IdentifierToken then
|
|---|
| 62 | if token.string == name then
|
|---|
| 63 | message2 := message + String(iline);
|
|---|
| 64 | (delimiter, nextIndex) := Strings.scanDelimiter(line, nextIndex, {"="}, message2);
|
|---|
| 65 | (result, nextIndex) := expression(line, nextIndex, message2);
|
|---|
| 66 | (delimiter, nextIndex) := Strings.scanDelimiter(line, nextIndex, {";", ""}, message2);
|
|---|
| 67 | Strings.scanNoToken(line, nextIndex, message2);
|
|---|
| 68 | found := true;
|
|---|
| 69 | else
|
|---|
| 70 | iline := iline + 1;
|
|---|
| 71 | end if;
|
|---|
| 72 | else
|
|---|
| 73 | Strings.syntaxError(line, nextIndex, "Expected identifier " + message + String(iline));
|
|---|
| 74 | end if;
|
|---|
| 75 | if not cache then
|
|---|
| 76 | (line, endOfFile) := readLineWithoutCache(fileName, iline);
|
|---|
| 77 | else
|
|---|
| 78 | (line, endOfFile) := Streams.readLine(fileName, iline);
|
|---|
| 79 | end if;
|
|---|
| 80 | end while;
|
|---|
| 81 | // skip line
|
|---|
| 82 | // name found, get value of "name = value;"
|
|---|
| 83 | // wrong name, skip line
|
|---|
| 84 | // wrong token
|
|---|
| 85 | // read next line
|
|---|
| 86 | /*
|
|---|
| 87 | if not cache then
|
|---|
| 88 | Streams.close(fileName);
|
|---|
| 89 | end if;
|
|---|
| 90 | */
|
|---|
| 91 | if not found then
|
|---|
| 92 | Streams.error("Parameter \"" + name + "\" not found in file \"" + fileName + "\"");
|
|---|
| 93 | end if;
|
|---|
| 94 | annotation(Documentation(info = "<html>
|
|---|
| 95 | <h4>Syntax</h4>
|
|---|
| 96 | <blockquote><pre>
|
|---|
| 97 | result = <b>readRealParameter</b>(fileName, name);
|
|---|
| 98 | </pre></blockquote>
|
|---|
| 99 | <h4>Description</h4>
|
|---|
| 100 | <p>
|
|---|
| 101 | This function demonstrates how a function can be implemented
|
|---|
| 102 | that reads the value of a parameter from file. The function
|
|---|
| 103 | performs the following actions:
|
|---|
| 104 | </p>
|
|---|
| 105 | <ol>
|
|---|
| 106 | <li> It opens file \"fileName\" and reads the lines of the file.</li>
|
|---|
| 107 | <li> In every line, Modelica line comments (\"// ... end-of-line\")
|
|---|
| 108 | are skipped </li>
|
|---|
| 109 | <li> If a line consists of \"name = expression\" and the \"name\"
|
|---|
| 110 | in this line is identical to the second argument \"name\"
|
|---|
| 111 | of the function call, the expression calculator Examples.expression
|
|---|
| 112 | is used to evaluate the expression after the \"=\" character.
|
|---|
| 113 | The expression can optionally be terminated with a \";\".</li>
|
|---|
| 114 | <li> The result of the expression evaluation is returned as
|
|---|
| 115 | the value of the parameter \"name\". </li>
|
|---|
| 116 | </ol>
|
|---|
| 117 | <h4>Example</h4>
|
|---|
| 118 | <p>
|
|---|
| 119 | On file \"test.txt\" the following lines might be present:
|
|---|
| 120 | </p>
|
|---|
| 121 | <blockquote><pre>
|
|---|
| 122 | // Motor data
|
|---|
| 123 | J = 2.3 // inertia
|
|---|
| 124 | w_rel0 = 1.5*2; // relative angular velocity
|
|---|
| 125 | phi_rel0 = pi/3
|
|---|
| 126 | </pre></blockquote>
|
|---|
| 127 | <p>
|
|---|
| 128 | The function returns the value \"3.0\" when called as:
|
|---|
| 129 | </p>
|
|---|
| 130 | <blockquote><pre>
|
|---|
| 131 | readRealParameter(\"test.txt\", \"w_rel0\")
|
|---|
| 132 | </pre></blockquote>
|
|---|
| 133 | </html>"));
|
|---|
| 134 | end readRealParameter;
|
|---|
| 135 |
|
|---|
| 136 | function expression "Expression interpreter that returns with the position after the expression (expression may consist of +,-,*,/,^,(),sin(), cos(), tan(), sqrt(), asin(), acos(), atan(), exp(), log(), pi"
|
|---|
| 137 | import Modelica.Utilities.Types;
|
|---|
| 138 | import Modelica.Utilities.Strings;
|
|---|
| 139 | import Modelica.Math;
|
|---|
| 140 | import Modelica.Constants;
|
|---|
| 141 | input String string "Expression that is evaluated";
|
|---|
| 142 | input Integer startIndex = 1 "Start scanning of expression at character startIndex";
|
|---|
| 143 | input String message = "" "Message used in error message if scan is not successful";
|
|---|
| 144 | output Real result "Value of expression";
|
|---|
| 145 | output Integer nextIndex "Index after the scanned expression";
|
|---|
| 146 | protected
|
|---|
| 147 | function term "Evaluate term of an expression"
|
|---|
| 148 | extends Modelica.Icons.Function;
|
|---|
| 149 | input String string;
|
|---|
| 150 | input Integer startIndex;
|
|---|
| 151 | input String message = "";
|
|---|
| 152 | output Real result;
|
|---|
| 153 | output Integer nextIndex;
|
|---|
| 154 | protected
|
|---|
| 155 | Real result2;
|
|---|
| 156 | Boolean scanning = true;
|
|---|
| 157 | String opString;
|
|---|
| 158 | algorithm
|
|---|
| 159 | // scan for "primary * primary" or "primary / primary"
|
|---|
| 160 | (result, nextIndex) := primary(string, startIndex, message);
|
|---|
| 161 | while scanning loop
|
|---|
| 162 | (opString, nextIndex) := Strings.scanDelimiter(string, nextIndex, {"*", "/", "^", ""}, message);
|
|---|
| 163 | if opString == "" then
|
|---|
| 164 | scanning := false;
|
|---|
| 165 | else
|
|---|
| 166 | (result2, nextIndex) := primary(string, nextIndex, message);
|
|---|
| 167 | result := if opString == "*" then result * result2 else if opString == "/" then result / result2 else result ^ result2;
|
|---|
| 168 | end if;
|
|---|
| 169 | end while;
|
|---|
| 170 | end term;
|
|---|
| 171 |
|
|---|
| 172 | function primary "Evaluate primary of an expression"
|
|---|
| 173 | extends Modelica.Icons.Function;
|
|---|
| 174 | input String string;
|
|---|
| 175 | input Integer startIndex;
|
|---|
| 176 | input String message = "";
|
|---|
| 177 | output Real result;
|
|---|
| 178 | output Integer nextIndex;
|
|---|
| 179 | protected
|
|---|
| 180 | Types.TokenValue token;
|
|---|
| 181 | Real result2;
|
|---|
| 182 | String delimiter;
|
|---|
| 183 | String functionName;
|
|---|
| 184 | Real pi = Constants.pi;
|
|---|
| 185 | algorithm
|
|---|
| 186 | (token, nextIndex) := Strings.scanToken(string, startIndex, unsigned= true);
|
|---|
| 187 | if token.tokenType == Types.TokenType.DelimiterToken and token.string == "(" then
|
|---|
| 188 | (result, nextIndex) := expression(string, nextIndex, message);
|
|---|
| 189 | (delimiter, nextIndex) := Strings.scanDelimiter(string, nextIndex, {")"}, message);
|
|---|
| 190 | elseif token.tokenType == Types.TokenType.RealToken then
|
|---|
| 191 | result := token.real;
|
|---|
| 192 | elseif token.tokenType == Types.TokenType.IntegerToken then
|
|---|
| 193 | result := token.integer;
|
|---|
| 194 | elseif token.tokenType == Types.TokenType.IdentifierToken then
|
|---|
| 195 | if token.string == "pi" then
|
|---|
| 196 | result := pi;
|
|---|
| 197 | else
|
|---|
| 198 | functionName := token.string;
|
|---|
| 199 | (delimiter, nextIndex) := Strings.scanDelimiter(string, nextIndex, {"("}, message);
|
|---|
| 200 | (result, nextIndex) := expression(string, nextIndex, message);
|
|---|
| 201 | (delimiter, nextIndex) := Strings.scanDelimiter(string, nextIndex, {")"}, message);
|
|---|
| 202 | if functionName == "sin" then
|
|---|
| 203 | result := Math.sin(result);
|
|---|
| 204 | elseif functionName == "cos" then
|
|---|
| 205 | result := Math.cos(result);
|
|---|
| 206 | elseif functionName == "tan" then
|
|---|
| 207 | result := Math.tan(result);
|
|---|
| 208 | elseif functionName == "sqrt" then
|
|---|
| 209 | if result < 0.0 then
|
|---|
| 210 | Strings.syntaxError(string, startIndex, "Argument of call \"sqrt(" + String(result) + ")\" is negative.\n" + "Imaginary numbers are not supported by the calculator.\n" + message);
|
|---|
| 211 | end if;
|
|---|
| 212 | result := sqrt(result);
|
|---|
| 213 | elseif functionName == "asin" then
|
|---|
| 214 | result := Math.asin(result);
|
|---|
| 215 | elseif functionName == "acos" then
|
|---|
| 216 | result := Math.acos(result);
|
|---|
| 217 | elseif functionName == "atan" then
|
|---|
| 218 | result := Math.atan(result);
|
|---|
| 219 | elseif functionName == "exp" then
|
|---|
| 220 | result := Math.exp(result);
|
|---|
| 221 | elseif functionName == "log" then
|
|---|
| 222 | if result <= 0.0 then
|
|---|
| 223 | Strings.syntaxError(string, startIndex, "Argument of call \"log(" + String(result) + ")\" is negative.\n" + message);
|
|---|
| 224 | end if;
|
|---|
| 225 | result := log(result);
|
|---|
| 226 | else
|
|---|
| 227 | Strings.syntaxError(string, startIndex, "Function \"" + functionName + "\" is unknown (not supported)\n" + message);
|
|---|
| 228 | end if;
|
|---|
| 229 | end if;
|
|---|
| 230 | else
|
|---|
| 231 | Strings.syntaxError(string, startIndex, "Invalid primary of expression.\n" + message);
|
|---|
| 232 | end if;
|
|---|
| 233 | end primary;
|
|---|
| 234 |
|
|---|
| 235 | Real result2;
|
|---|
| 236 | String signOfNumber;
|
|---|
| 237 | Boolean scanning = true;
|
|---|
| 238 | String opString;
|
|---|
| 239 | algorithm
|
|---|
| 240 | // scan for optional leading "+" or "-" sign
|
|---|
| 241 | (signOfNumber, nextIndex) := Strings.scanDelimiter(string, startIndex, {"+", "-", ""}, message);
|
|---|
| 242 | // scan for "term + term" or "term - term"
|
|---|
| 243 | (result, nextIndex) := term(string, nextIndex, message);
|
|---|
| 244 | if signOfNumber == "-" then
|
|---|
| 245 | result := -result;
|
|---|
| 246 | end if;
|
|---|
| 247 | while scanning loop
|
|---|
| 248 | (opString, nextIndex) := Strings.scanDelimiter(string, nextIndex, {"+", "-", ""}, message);
|
|---|
| 249 | if opString == "" then
|
|---|
| 250 | scanning := false;
|
|---|
| 251 | else
|
|---|
| 252 | (result2, nextIndex) := term(string, nextIndex, message);
|
|---|
| 253 | result := if opString == "+" then result + result2 else result - result2;
|
|---|
| 254 | end if;
|
|---|
| 255 | end while;
|
|---|
| 256 | annotation(Documentation(info = "<html>
|
|---|
| 257 | <h4>Syntax</h4>
|
|---|
| 258 | <blockquote><pre>
|
|---|
| 259 | result = <b>expression</b>(string);
|
|---|
| 260 | (result, nextIndex) = <b>expression</b>(string, startIndex=1, message=\"\");
|
|---|
| 261 | </pre></blockquote>
|
|---|
| 262 | <h4>Description</h4>
|
|---|
| 263 | <p>
|
|---|
| 264 | This function is nearly the same as Examples.<b>calculator</b>.
|
|---|
| 265 | The essential difference is that function \"expression\" might be
|
|---|
| 266 | used in other parsing operations: After the expression is
|
|---|
| 267 | parsed and evaluated, the function returns with the value
|
|---|
| 268 | of the expression as well as the position of the character
|
|---|
| 269 | directly after the expression.
|
|---|
| 270 | </p>
|
|---|
| 271 | <p>
|
|---|
| 272 | This function demonstrates how a simple expression calculator
|
|---|
| 273 | can be implemented in form of a recursive decent parser
|
|---|
| 274 | using basically the Strings.scanToken(..) and scanDelimiters(..)
|
|---|
| 275 | function. There are 2 local functions (term, primary) that
|
|---|
| 276 | implement the corresponding part of the grammar.
|
|---|
| 277 | </p>
|
|---|
| 278 | <p>
|
|---|
| 279 | The following operations are supported (pi=3.14.. is a predefined constant):
|
|---|
| 280 | </p>
|
|---|
| 281 | <pre>
|
|---|
| 282 | +, -
|
|---|
| 283 | *, /, ^
|
|---|
| 284 | (expression)
|
|---|
| 285 | sin(expression)
|
|---|
| 286 | cos(expression)
|
|---|
| 287 | tan(expression)
|
|---|
| 288 | sqrt(expression)
|
|---|
| 289 | asin(expression)
|
|---|
| 290 | acos(expression)
|
|---|
| 291 | atan(expression)
|
|---|
| 292 | exp(expression)
|
|---|
| 293 | log(expression)
|
|---|
| 294 | pi
|
|---|
| 295 | </pre>
|
|---|
| 296 | <p>
|
|---|
| 297 | The optional argument \"startIndex\" defines at which position
|
|---|
| 298 | scanning of the expression starts.
|
|---|
| 299 | </p>
|
|---|
| 300 | <p>
|
|---|
| 301 | In case of error,
|
|---|
| 302 | the optional argument \"message\" is appended to the error
|
|---|
| 303 | message, in order to give additional information where
|
|---|
| 304 | the error occured.
|
|---|
| 305 | </p>
|
|---|
| 306 | <p>
|
|---|
| 307 | This function parses the following grammaer
|
|---|
| 308 | </p>
|
|---|
| 309 | <pre>
|
|---|
| 310 | expression: [ add_op ] term { add_op term }
|
|---|
| 311 | add_op : \"+\" | \"-\"
|
|---|
| 312 | term : primary { mul_op primary }
|
|---|
| 313 | mul_op : \"*\" | \"/\" | \"^\"
|
|---|
| 314 | primary : UNSIGNED_NUMBER
|
|---|
| 315 | | pi
|
|---|
| 316 | | ( expression )
|
|---|
| 317 | | functionName( expression )
|
|---|
| 318 | function : sin
|
|---|
| 319 | | cos
|
|---|
| 320 | | tan
|
|---|
| 321 | | sqrt
|
|---|
| 322 | | asin
|
|---|
| 323 | | acos
|
|---|
| 324 | | atan
|
|---|
| 325 | | exp
|
|---|
| 326 | | log
|
|---|
| 327 | </pre>
|
|---|
| 328 | <p>
|
|---|
| 329 | Note, in Examples.readRealParameter it is shown, how the expression
|
|---|
| 330 | function can be used as part of another scan operation.
|
|---|
| 331 | </p>
|
|---|
| 332 | <h4>Example</h4>
|
|---|
| 333 | <blockquote><pre>
|
|---|
| 334 | expression(\"2+3*(4-1)\"); // returns 11
|
|---|
| 335 | expression(\"sin(pi/6)\"); // returns 0.5
|
|---|
| 336 | </pre></blockquote>
|
|---|
| 337 | </html>"));
|
|---|
| 338 | end expression;
|
|---|
| 339 |
|
|---|
| 340 | function readLineWithoutCache "Reads a line of text from a file without cahing and returns it in a string"
|
|---|
| 341 | input String fileName "Name of the file that shall be read" annotation(Dialog(__Dymola_loadSelector(filter = "Text files (*.txt; *.dat)", caption = "Open file in which Real parameters are present")));
|
|---|
| 342 | input Integer lineNumber(min = 1) "Number of line to read";
|
|---|
| 343 | output String string "Line of text";
|
|---|
| 344 | output Boolean endOfFile "If true, end-of-file was reached when trying to read line";
|
|---|
| 345 |
|
|---|
| 346 | external "C" string= ReadLine(fileName, lineNumber, endOfFile);
|
|---|
| 347 | annotation(Include = "
|
|---|
| 348 | #ifndef ReadLine_C
|
|---|
| 349 | #define ReadLine_C
|
|---|
| 350 | # include <stdio.h>
|
|---|
| 351 | # include <string.h>
|
|---|
| 352 | extern void ModelicaFormatError(const char* string,...);
|
|---|
| 353 | extern char* ModelicaAllocateString(size_t len);
|
|---|
| 354 | #ifndef MAXLEN
|
|---|
| 355 | #define MAXLEN 133
|
|---|
| 356 | #endif
|
|---|
| 357 | const char* ReadLine(const char *fileName, int lineNumber, int* endOfFile)
|
|---|
| 358 | {
|
|---|
| 359 | FILE* fp;
|
|---|
| 360 | char c, strline[MAXLEN];
|
|---|
| 361 | char* line;
|
|---|
| 362 | int iLine;
|
|---|
| 363 | size_t lineLen;
|
|---|
| 364 | if ((fp=fopen(fileName,\"r\")) == NULL)
|
|---|
| 365 | {
|
|---|
| 366 | ModelicaFormatError(\"ReadLine: File %s not found!\\n\",fileName);
|
|---|
| 367 | return \"\";
|
|---|
| 368 | }
|
|---|
| 369 | iLine=0;
|
|---|
| 370 | while (++iLine <= lineNumber)
|
|---|
| 371 | {
|
|---|
| 372 | c=fgetc(fp);
|
|---|
| 373 | lineLen=1;
|
|---|
| 374 | while (c != '\\n' && c != '\\r' && c != EOF)
|
|---|
| 375 | {
|
|---|
| 376 | if (lineLen < MAXLEN)
|
|---|
| 377 | {
|
|---|
| 378 | strline[lineLen-1]=c;
|
|---|
| 379 | c=fgetc(fp);
|
|---|
| 380 | lineLen++;
|
|---|
| 381 | }
|
|---|
| 382 | else
|
|---|
| 383 | {
|
|---|
| 384 | fclose(fp);
|
|---|
| 385 | ModelicaFormatError(\"ReadLine: Line %d of file %s truncated!\\n\",iLine,fileName);
|
|---|
| 386 | return \"\";
|
|---|
| 387 | }
|
|---|
| 388 | }
|
|---|
| 389 | if (c == EOF && iLine < lineNumber)
|
|---|
| 390 | {
|
|---|
| 391 | fclose(fp);
|
|---|
| 392 | line=ModelicaAllocateString(0);
|
|---|
| 393 | *endOfFile=1;
|
|---|
| 394 | return line;
|
|---|
| 395 | }
|
|---|
| 396 | }
|
|---|
| 397 | fclose(fp);
|
|---|
| 398 | strline[lineLen-1]='\0';
|
|---|
| 399 | line=ModelicaAllocateString(lineLen);
|
|---|
| 400 | strcpy(line, strline);
|
|---|
| 401 | *endOfFile=0;
|
|---|
| 402 | return line;
|
|---|
| 403 | }
|
|---|
| 404 | #endif
|
|---|
| 405 | ", Documentation(info = "<html>
|
|---|
| 406 | <h4>Syntax</h4>
|
|---|
| 407 | <blockquote><pre>
|
|---|
| 408 | (string, endOfFile) = <b>readLine</b>(fileName, lineNumber)
|
|---|
| 409 | </pre></blockquote>
|
|---|
| 410 | <h4>Description</h4>
|
|---|
| 411 | <p>
|
|---|
| 412 | Function <b>readLine</b>(..) opens the given file, reads enough of the
|
|---|
| 413 | content to get the requested line, and returns the line as a string.
|
|---|
| 414 | Lines are separated by LF or CR-LF; the returned string does not
|
|---|
| 415 | contain the line separator.
|
|---|
| 416 | </p>
|
|---|
| 417 | <p>
|
|---|
| 418 | If lineNumber > countLines(fileName), an empty string is returned
|
|---|
| 419 | and endOfFile=true. Otherwise endOfFile=false.
|
|---|
| 420 | </p>
|
|---|
| 421 | </html>"));
|
|---|
| 422 | end readLineWithoutCache;
|
|---|
| 423 |
|
|---|
| 424 | annotation(uses(Modelica(version="3.2.2")));
|
|---|
| 425 | end KeyWordIO;
|
|---|