3
Data: Numbers and Strings

Developing and mastering the natural language has brought humankind a decisive advantage on our planet. Interpreting sounds or written signs is the first step. For a computer, this interpretation is not as soft and flexible as it is for human beings. In any real application, the first obstacle when reading a number or a piece of text is to correctly include it into the processing flow.

A single sign may mean different things: figures 7 and 10 can be numbers, but in “Ronaldo wears the 7, and Maradona the 10”, it is just nonsense to add 7 and 10.

Different words may mean the same thing: “Général De Gaulle” and “Charles de Gaulle”, or simply “Jean Pierre” and “Jean-Pierre”.

An illustration is given with the electoral data in Part 3: the difficulties in identifying a same single candidate when the name can be differently written out in two different files.

In this chapter, we visit “numbers” and “strings”, and in particular the use of:

  • – concatenation operator (+), and comparisons with type string variables;
  • – methods of the object String and String.prototype;
  • – regular expressions and the object RegExp.

3.1. Handling numbers

3.1.1. Literal notation of type “number” variables

The literal notation of numbers can take several forms:

 4.2             encodes the real number 4.2
 -3e-5           encodes -0.00003 (scientific notation)
 0x0f            encodes 15 (hexadecimal notation)
 0x0g            SyntaxError: identifier starts immediately after numeric literal
                 // only characters [0-9, a-f] are allowed after prefix 0x0

There are two built-in values whose typeof is "number": NaN, Infinity1.

Table 3.1. Examples of numeric expression evaluations

Evaluation Result Type
x = -0x0f / -3e2; 0.05 Number
x = 12 / -0; -Infinity Number
x = -100000 * Infinity; Infinity Number
x = Infinity / Infinity; NaN Number

NOTE.– How to know if a value is a number? The value NaN is of type "number", therefore, it is not enough to check if a variable is really different from any number:

let t = "", x = 12/0; x = 2*x / x;
if(typeof x !== "number"){t = x +" is not a number";}
else {t = x +" is ok";}
                         // NaN is ok (misleading answer!)

The built-in method isNan() returns true for any value that is not a number, including NaN.

 if(isNaN(x)){t = x +" is really not a number";}
 else {t = x +" is really ok";}
                        // NaN is really not a number

3.1.2. Arithmetic operators

  • Classical 4 arithmetic operators: +, -, *, /
    let x = (20 +5) * 4 / 100; // -> 1

    It looks classical but with the operator +, if one operand is a “string” (see next), the left to right evaluation will be modified (with respect to the parentheses).

  • Modulo operator: %
    let x = 20 % 3; // -> 2
  • Exponentiation operator: **(any number as exponent, since 2016)
    let x = 2 ** (1/2); // ->image: 1.4142135623730951

    WARNING.–Beware of parentheses (suggestion: be explicit).

     2 ** 3 ** 2         // 512:
     2 ** (3 ** 2)       // 512
     (2 ** 3) ** 2       // 64
     (-2) ** 2           // 4
     -2 ** 2             // Syntax Error
     -(2 ** 2)           // -4
  • Incrementation/decrementation operators (unary): ++, –
    • – // Postfix use
      let x = 3, y = x++; // y = 3, x = 4
    • – // Prefix use
      let a = 2, b = ++a; // a = 3, b = 3
      let x = 0, y = x++, z = ++x;          // y=?, z=?, x=
      
  • Combined numeric and assign operators: += , -=, *= , /=, %=
     x += 5;       // equivalent: x = x + 5;
  • Unary operators +, –

    Besides keeping (+) or inversing (–) the sign of the variable, the unary operator makes an implicit type conversion whenever mandatory (see the following).

    let x = 3,
          y = +x,         // -> 3, looks useless,
                          // but informs the engine that typeof y === "number"
          z = -x;         // -> -3
    

WARNING.– Polymorphism of +.

The addition operator + is also a concatenation operator when type is string. As soon as a string is found, the rest of the expression is cast to a string, which changes the behavior of next + operators.

          console.log("3" + 4 + 5);                // "345"
          console.log(3 + 4 + "5");                // "75"
          console.log(3 + 4 + 5);                  // 12
          console.log( 248 + "" );          // converts number -> string
          console.log(true/2 + "false"); // exercice! do it yourself

3.1.3. Math operations using the methods of the object Math

The Math object is a built-in object that includes several properties and methods, facilitating the use of mathematical constants and usual mathematical functions.

Table 3.2. Mathematical constants as properties of the Math object

Properties Description Value
Math.E Number e 2.718281828459045
Math.th.LN10 Natural logarithm of 10 2.302585092994046
Math.LN2 Number natural logarithm of 2 0.6931471805599453
Math.LOGlOE Number base 10 logarithm of e 0.4342944819032518
Math.LOG2E Number base 2 logarithm of e 1.4426950408889634
Math.PI Number pi 3.141592653589793
Math.SQRT1_2 Number 0.7071067811865476
Math.SQRT2 Number 1.4142135623730951

Table 3.3. Usual mathematical functions as methods of the Math object

Methods Description
Math.abs() Absolute value of (x)
Math.sign() Sign of (x) can be: 1, –1, 0, –0, NaN
Math.min()
Math.max()
Minimum, maximum of (x, y)
Math.floor()
Math.ceil()
Integer, immediately below or above (x)
Math.round()
Math.trunc()
Math.fround()
Rounded integer value or simple truncature of (x)
ou Float simple precision rounded value
Math.sqrt()
Math.cbrt()
Square or cubic root of (x)
Math.hypot () Hypotenuse of (n, m) may generalizes to >2 arguments
Math.pow() x Power of y (x, y)
Math.cos()
Math.sin()
Math.tan()
Direct trigonometric functions of (x) as "radian": see example below
for the conversion [–180,180] -> [-π, π]
Math.acos()
Math.asin()
Math.atan()
Math.atan2()
Inverse trigonometric functions, returns in [- π, π]
+ atan2(a,b) = tangent arc of the ratio a/b
Math.cosh()
Math.sinh()
Math.tanh()
Direct hyperbolic functions
Ex: Math.cosh(x)= ½(ex+e-x)
Math.acosh()
Math.asinh()
Math.atanh()
Inverse hyperbolic functions
Ex: x≥1, arcosh(x)=ln(x+ √ {x2 - 1})
Math.exp()
Math.expm1()
Exponential of ex, or ex - 1 of (x)
Math.log()
Math.log1p()
Math.log10()
Math.log2()
Logarithms: natural logarithm of x, natural logarithm of x+1,
decimal logarithm, base 2 logarithm of (x)

3.1.3.1 Examples

  • Converting an angle value from degree to radian
      function degToRag(deg) {return deg * Math.PI/180;}
  • Getting a random number inside a given positive interval [1, N]
      function randomN(n) {return Math.floor( n * Math.random(n) );}

3.1.4. Evaluation in the “numerical context” versus “boolean context”

When the expression starts with an item of type "number", the rest of the expression is evaluated in “numerical context”, and a boolean value will be converted to either 1, if true or 0 if false.

Example:

let a = true;                         let b = false;
 let y = 1 + a - 12 * b;// -> 2
     y = +a; // -> 1                         y = +b; // -> 0
     y = -a; // -> -1                        y = -b; // -> 0

On the other hand, in “boolean context”, any value of type "number" is converted to true, including Infinity and -Infinity, with the exception of the values 0, -0 and NaN, which are converted to false:

let x = 0; if(x){ /* code is not executed */ }
     x = 12; if(x){ /* code is executed */ }

If, anywhere later in the evaluation of the expression, a type "string" is met, the evaluation switches to “string context”, and values true and false become string values:

 txt = 2 - true +" equals "+ (1 + false);           // 1 equals 1
 txt = 2 - true +" equals "+ 1 + false;             // 1 equals 1false

3.2. Handling character strings

3.2.1. Literal notation of strings

A string literal notation uses quotation marks that can be of three kinds:

– double quotes: "Candidate";
– simple quotes: '500px';
– or back quotes (aka. backticks): 'the value of x is ${x}'.

Simple or double quotes play the same role. Combining them is useful when quotes must be used in the string that we write in the literal notation, for instance:

 "Candidate 'Jean' in Department '2A'"

If necessary, the inner quotes can be escaped:

 "Candidate 'Jean, dit "Jeannot"' in Department 2A'"
  • – the associated type is “string”
     console.log( typeof "Candidat");       // -> string
     console.log( typeof "");               // -> string
    
  • – validity of the characters used inside the literal notation

A string must be composed of valid unicode characters, including whitespaces, special characters, etc., and must be correctly enclosed by appropriate quotes:

 "the letter Π stands for 3.14159"

(the Greek letter 'pi' has unicode u03C0)

3.2.2. Backtick syntax, or template syntax, introduced by ES6

The introduction of this new syntax makes it possible to merge strings and values of expressions, which are evaluated themselves during the evaluation of the whole string. The syntax of the template part is ${..} and will trigger its evaluation:

const cssClassConteneur = "container";
let n = 0; // some index
let htm = '<div class="${cssClassConteneur+n++>"></div>';
// <div class="container0"></div>, and n = 1

3.2.3. Concatenation operator

The same sign + is used for number addition and for string concatenation. It is a “polymorphism issue” that is resolved by the script engine, when determining which context is at stake: either a boolean context (only if in a control) or a numerical context or a string context.

3.2.4. Resolving polymorphism issues with operator + in numerical or string context

  • – Let us assume we are not in a control (test in a branch or loop instruction).
  • – Switching to string context:

    If the + sign is used as a binary operator between two operands, one of which being a string, then it is interpreted as the “concatenation” operator:

    let str = "Candidat"; console.log(str + 1);              // -> Candidat1
                             console.log(str + 1+1);         // -> Candidat11
                             console.log(str + (1+1));       // -> Candidat2
                             console.log (str+ 1-1);         // -> NaN
             //beware!: the – sign doesn't work with a string, and
            //the evaluation returns NaN whose type is a number!
  • – Switching to numerical context:

    If the + sign is used in first position, as a unary operator, it casts the next variable to a valid number or 'NaN' (also a number). Same behavior with the unary –sign:

                console.log (+1+str);    // -> 1Candidat
                console.log (+"1"+2);    // -> 3
                console.log (-"1"+2);    // -> 1
                console.log (+"a"+2);    // -> NaN

EXERCISE.- The inattention leads to the unexpected (explained by the next two lines):

 (typeof 4 ==="number") + "two".length + typeof 4;// 4number
 (typeof 4 ==="number") +""+ "two".length +""+ typeof 4;
                                              // true3number

3.2.5. Behavior of the relational and equality operators

  • – Between two numbers, it returns the expected true or false value.
  • – Between a number and a string, a comparison always returns false:
         let str = "abc", n = 2;
         console.log(n > str ); // false )  both cases return false
         console.log(n < str ); // false )  for uncompatible types
    
  • – Between two strings, the comparison complies with the unicode order:

    The figures [0-9] are lower than all letters:

     let n = 10, m = 2, str = "abc";
     console.log(n > m);              // true (comparison of numbers)
     console.log(n+str > str+n );     // false <- Unicode rule
     console.log(n+str < str+n );     // true <- Unicode rule
     console.log(n+str > m+str );     // false <- Unicode rule below is false
    // warning: '10abc' > '2abc'     is false
    // because the "2" is greater than the "1" of "10"
    

WARNING.– Difference between weak and strict equality operators: ==, === (resp. inequality: !=, !==)

 console.log(0x0f == "15");           // true     <=same evaluation
 console.log(0x0f === "15");          // false    <=different types

This is tedious, for sure, but we must face it in the reality of uses.

3.2.6. Various facets of string-related issues in a sample application

The example of enumerating the French “départements” illustrates many issues that are worth mentioning for a data-oriented use of JavaScript. This example is part of one of the applications mentioned in Part 3.

Every department has a name and a code, but the alphabetic order of the names does not exactly follow the numeric order of the codes for various historic reasons:

"Ain": 1, "Aisne": 2, etc … "Savoie (Haute)": 74, "Paris": 75 (was named "Seine" before 1968), etc. … "Val-d'Oise":95, plus the “ultramar” departments, whose codes start with a Z: "Guadeloupe":"ZA", etc.

Another issue comes from the partitioning, in 1976, of the “Corse” department into “Haute-Corse” and “Corse-du-Sud”, with codes “2A” and “2B”. The code “20” for “Corse” is not used anymore. All these added oddities hamper a direct sequential encoding of the departments. Let us process the following example:

const deps = []; // array for department's data
deps [0]: index 0, name "Ain", code 0 => code = index +1
deps [1]: index 1, name "Aisne", code 2 => code = index +1

deps [19]: index 19, name "Haute-Corse", code 2A => code =?f(index)
deps [20]: index 20, name "Corse-du-Sud", code 2B => code =?f(index)
deps [21]: index 21, name "Cote-d'Or", code 21 => code = index

Therefore, we have four situations:

  • – from code “1” to “19”, index = (+code)-1;
  • – from “21” to “95”, index = (+code);
  • – special case for the two Corsica departments (codes 2A and 2B)
  • – special case for the “ultramarin” departments (codes ZA to ZZ)

NOTE.– We use (+code) to force the string code to return a numeric type.

Here is a possible solution for computing the index as a function of the code:

 const ultramar =
        [95,"ZA","ZB","ZC","ZD","ZM","ZN","ZP","ZS","ZW","ZX","ZZ"];
 function getIndexFromCode(code){       // typeof code: 'string'
     let ind;
     const patt1 = /[A-Z]/g;
     if(code.match(patt1)){        // typeof +code not numerical (1)
       if(code==="2A") ind=19; if(code==="2B") ind=20; //Corse   (2)
       if(code.charAt(0)==="Z"){         // see array ultramar                 (3)
         ind = ultramar[0] + ultramar.indexOf(code);
       }
     }else{                              // type of +code is number (4)
       ind = parseInt(code); // car code est un string
       if(ind<20) ind -= 1; // de 1-Ain a 19-Correze
     }
     return ind;
 }

The logic of the function is:

  • – check if code contains alpha characters;
  • – if yes: deal with the “Corse” department (easy) or:
  • – with “ultramar” departments, using the dedicated array of “code” values:

    ultramar[0] = 95; is the number to be added in the instruction: ind = ultramar[0] + ultramar.indexOf(cod); where the method indexOf returns the index of the value in the array.

  • – otherwise: code can be converted to a number with the method parseInt, which is a method of the global object, such as isNaN(), which we have seen in the previous section.

Let us detail the parseInt and similar parseFloat methods.

Table 3.4. Conversion methods parseInt and parseFloat

parseInt Result parseFloat Result
parseInt("") NaN parseFloat("") NaN
parseInt("042.5km") 42 parseFloat("042.5km") 42.5
parseInt("077") 77 (base 10) parseFloat("0xF") 0 (no hexa)
parseInt("0xF") 63(base 8) parseFloat("2.5e6") 2500000
parseInt("077",8) 15 hexa

NOTE.– The methods parseInt and parseFloat allow us to convert to a numeric value, and the methods isNan and isFinite allow us to check if the variable has one of these special values. Since ES6, these four methods are added to the object Number. For instance, we can use Number.parseInt(code, 10).

3.3. The String.prototype methods

3.3.1. The need for preprocessing before comparison

Minor differences, which the human eye directly assimilates, are potential error sources when processing data with a computer. For instance, “JavaScript” and “Javascript” are not two different languages, and it is mandatory to preprocess strings before comparing them, for instance removing extra spaces or tabs, etc.

Example (using string methods toLowerCase and trim):

const str1 = "JavaScript", str2 = " Javascript ";
let s1 = str1.toLowerCase().trim();      // "javascript"
let s2 = str2.toLowerCase().trim();      // "javascript"
console.log( str1 === str2 );   // false
console.log( s1 === s2 );       // true

Some natural languages use accentuated characters that, in some circumstances, we may decide to ignore. The string method localeCompare allows us to precisely define what to take into account or not: it can be used with numbers (decimal dot versus comma), dates (several actual formats) and strings with accents:

let strl = "abé", str2 = "àBe";
const compareOpts = {"usage":"search", "sensitivity":"base"}
let n = strl.localeCompare(str2, "fr", compareOpts);
// n = 0 means equal [, l greater, -l lower]

The optional arguments in localeCompare(str2 [,locale,opts]) specify the language (here: "fr") and which set of rules to apply for preprocessing:

  • – "usage":" search" speeds up the comparison if only equality is needed, for no sort is performed, else the default is: "sort" ;
  • – "sensitivity":"base", means that accents and upper/lower cases are ignored: a=A, a=à, a≠b, else the default is "variant" (i.e. a≠A, a≠à, a≠b).

The method localeCompare solves the lowercase and the accent problems at once. More particular differences can be solved by using the method replace when combined with appropriate regular expressions.

3.3.2. Handling partial comparisons

Partial comparisons may allow us to handle groups of situations. For instance, we can regroup the ultramar departments, or Corsica, by checking only the first character of the string, if it is a “Z”, or if it is a “2” followed by a letter:

const corse_hte = "2A", corse_sud = "2B", guadeloupe = "ZA";
let str = guadeloupe; console.log(str.charAt(0) === "Z" ); // true
str = corse_sud; console.log(str.charAt(0) === "2" &&
str.charAt(1) >= "A"); // true: [0-9]<[A-Z]<[a-z]

3.3.3. Methods for handling strings

Table 3.5. Methods of the prototype of the object string

Methods Description Return
str.charAt () Returns the character at index (n) String
str. charcodsAt() Returns the UTF-16 code of that character Number
str. codePointAt() Returns the unicode point of that character Number
str.concat () Combines str with a second (str2) String
str.endsWith()
str.startsWith()
str.includes()
Checks if the string ends with (str2)
… starts with
… or containsBoolean
Boolean
str.indexOf()
str.lastIndexOf()
Index of the first character = (str), or –1 … or the last Number
str. localeCompare () Comparison according to local rules (–1,0,1)
str.matah() see RegExp Array
str. normal ize() Normalization unicode String
str.repeat () Repeats (n) and concats String
str.replace() see RegExp String
str.searah() Returns the index of the (pattern) or –1 Number
str.slice()
str.substr()
str.substring()
Returns a slice between (start, end)
< prefer slice
< prefer slice
String
str.split() Splits into an array of strings, wrt (separator) Array
str.toLocaleLowerCase()
str.toLocaleUpperCase()
str.toLowerCase()
str.toUpperCase()
Returns a modified string String
str.trim() Returns a string without spaces around String

The built-in String object has been designed to support a bunch of methods, which will be delegated to any string, with the help of an associated object String.prototype. This delegation mechanism will be detailed later. Table 3.5 provides a list of the methods of the String.prototype object.

Let us detail some of the methods listed.

3.3.3.1. Slice(start, end)

The indices (start,end) are consistently handled, returning the empty string if start ≥ end, and allowing negative values to count backward from the end.

let str = 'The morning is upon us.'; // str.length : 23.
str.slice(1, 8);      // -> 'he morn'
str.slice(8, -2);     // -> 'ing is u'
str.slice(12);        // -> 'is upon us.' (up to the end)
str.slice(30);        // -> '' (… au-delà de 23)
str.slice(-3);        // -> 'us.' (up to the end)
str.slice(0, -2);     // -> 'The morning is upon u'

NOTE.– The methods slice, substr, substring are similar (competing for historical reasons). The method slice has the most consistent use of start, end and negative values: just avoid the other two.

3.3.3.2. Concat(second)

console.log("Good".concat(" Morning")); // Good Morning

3.3.3.3. Split(separator)

This is very useful for transforming a string into an array of strings controlled by the separator argument.

EXAMPLE.– Using split to process CSV data (“Comma Separated Values”).

  const schemaCSV = "Id#, Product, Unit Price";
 const schemaTable = schemaCSV.split(", ");
         // ["Id#", "Product", "Unit Price"]

The separator can be a string, such as that previously mentioned, or a regular expression (see the following) in which case, the use of parentheses has a meaning: “capturing parentheses”. The separator itself is added to the returned array.

EXAMPLE.–The regular expression searches the “digits” (code = d) and splits:

 const str = 'Hello 1 word. Sentence 2.';
 let split = str.split(/d/); // no parenthesis ->
         // split: ["Hello ", " word. Sentence ", "."]
split = str.split(/(d)/); // with parentheses ->
         // split: ["Hello ", "1", " word. Sentence ", "2", "."]

3.3.4. Regular expressions

Regular expressions are “patterns” that describe how a string is made of parts, and we can use them to retrieve any subchain made the same way.

Their esoteric aspect makes them repulsive, which leads to neglect them. But they are very useful in combination with the “string methods”: match, replace, search and split.

Regular expressions can be written as:

  • – in literal notation: const patt = /http/gm;.
  • – With constructor: const patt = new RegExp("http",flg);.

flg, optional, can be: let flg = "gm"; (which means: global + multiline)

Any object patt inherits from the object RegExp.prototype.

Table 3.6. Methods of RegExp.prototype

Methods Description Retour
patt.test() Checks the pattern in the string (str) Boolean
patt.exec() Returns an array depending on (str) Array

3.3.5. Evaluation and uses

  • – It is used directly as an object receiving a method, for example:
    const patt = /http/;            // pattern 'http', no modifier
    patt.test("http://www.w3shools.com");   // -> true
  • – Also as an argument of a string method, for example:
    const patt = /d/g; // pattern 'd', modifier : g = 'global'
    const str = "la date du 14 juillet<br>";
    let tab, msg = str;
    while ((tab = patt.exec(str)) !== null) {
            msg += 'match ' + tab[0] + ', at index: ' + tab.index +"<br>";
     }
    console.log( msg + str.replace(patt, "#"));
    /* displays:
            la date du 14 juillet
            match 1, at index: 11
            match 4, at index: 12
            la date du ## juillet
    

3.3.6. Some examples of useful patterns

const pattHexa = [0-9a-fA-F]+               // hexadecimal integer
const pattIPad = (d{1,3}.){3}d{1,3}      //simple IP address

Breaking down the hexadecimal pattern:

  • – 0–9: seeks for a digit;
  • – a–f: seeks for a letter between a and f, idem with A–F;
  • – the square brackets: seek for characters of one of the previous kind;
  • – the trailing + sign means any number of occurrences, at least one;
  • – for example: pattHexa.test("2Abd"); // -> true.

    Breaking down the IP address pattern:

  • – seeking for a group of 1–3 digits: use (d) for one digit and a repeat factor {1,3} between a minimum and a maximum. This gives: (d){1,3}, or simply d{1,3} because d is a primitive sign, it does not require the parentheses;
  • – seeking for three times such a group, followed by a dot, plus once more, without the dot: let us add the dot: (d{1,3}.) and the parentheses are required here, then again a repeat factor {3}, just three times, plus a fourth and last group: (d{1,3}.){3}d{1,3}

NOTE.– The dot is the sign for “any” character, therefore we must “escape” it: . to ensure that we are really seeking for a dot and not for any character.

3.3.7. General syntax of a regular expression

3.3.7.1. The “pattern”

This is delimited by slash signs / … / and can be followed by a modifier, which is one or several letters among which: “g” for global (every occurrence of the pattern in the string), “i” for insensitive to upper-/lowercase and “m” for multiline.

3.3.7.2. The characters

The characters that we expect to individually match can be any character, except the seven listed below, which must be “escaped” (backslashed), plus, depending on the context, the parentheses and square brackets:

?      .      $      +     *     /                  (..)     [..]
?     .     $     +    *    /     \           (..)   [..]

3.3.7.3. The classes of alternative signs

Instead of checking the signs individually, we can check a sign among a class of alternative signs delimited by brackets [..]. For example:

[abc] 	       means either a, b or c
[a-zA-Z]     means any character between a and z or between A and Z
  • – we can use shortcuts for some usual classes:
    d    instead of     [0-9],
    w    instead of      [a-zA-Z0-9_]
    s    instead of      [ 	
    vf]   spaces, tabs, line-feeds …
  • –we can negate a group with [^..];
  • – we negate a shortcut with the corresponding uppercase letter: D to negate d;
  • – we can form a subpattern by using parentheses (..) and we can number them from 1 to 9, for a further use, or avoid numbering them with a ? mark: (?:..);
  • – we can accept a repeated subpattern, exactly n times, at least minimum times, or between minimum and maximum times:      (..){n}

    (..){min,}      (..){min,max};

  • – [several more features can be found documented on the Internet …].

3.3.8. Combining RegExp and String.prototype methods

3.3.8.1. String.prototype.replace()

For example, multitarget reformating. For instance, we want to prepare a string that can be formatted later for HTML display or simple text display. This can be done easily by using some predefined custom mark (here: §§§) to tell when reformatting must occur. Then, different formats may be applied:

const str = "sub-paragraph 1§§§sub-paragraph 2§§§sub-paragraph 3";
let htm = "<p>" + str.replace(/§§§/g, "<br>"); + "</p>";
let txt = str.replace(/§§§/g, "
"); + "
";

3.3.8.2. String.prototype.match(), String.prototype.search()

These two methods are straightforward (see also metaprogramming with symbols in Chapter 7.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.184.223