JavaScriptのStringについて part2

# 初めに
前回で漏れた一部の文字列メソッドと、`JSON`も一緒にまとめてみました。

今回の参考資料です。

https://javascript.info/json

https://es6.ruanyifeng.com/?search=%E9%81%8D%E5%8E%86&x=7&y=8#docs/string

（「字符串的扩展」の1～4まで）

# Methods
## String.prototype.padStart()
目標の文字列が指定の長さに達していないとき、指定の長さまで先頭にほかの文字列を追加する。
```jsx
// Syntax
str.padStart(targetLength[, padString])
```
下はMDNの例です。

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart

```jsx
// console.log('123'.padStart(5, '0')); // '00123'

const fullNumber = '012345678910';
const lastFourDigits = fullNumber.slice(-4);
let maskedNumber = lastFourDigits.padStart(fullNumber.length, '*');
console.log(maskedNumber); // ********8910
```
少しもの足りなさを感じて正規表現で練習をしました。
```jsx
function maskedNum(str, unmaskedLength) {
  if (/^\d+$/.test(str)) {
    return '*'.repeat(str.length - unmaskedLength) + str.slice(-(unmaskedLength));
  }
  return 'Invalid input';
}
console.log(maskedNum('012345678910', 4)) // ********8910
console.log(maskedNum('a12345678910', 4)) // Invalid input
```

## String.prototype.padEnd()
`padStart()`とほぼ同じ仕組みです。長さが足りるまで末尾から文字列を追加する。
```jsx
// Syntax
str.padEnd(targetLength[, padString])
```

...でも何か柔軟性が足りないので別の練習になってしまいました。
```jsx
// console.log('abcdef'.padEnd(10, '.')); // abcdef....

function bannerString(str, targetLength) {
  if ((str.length - 1) <= (targetLength - 3)) {
    return str + '.'.repeat(3);
  }
  return str.slice(0, targetLength - 3) + '.'.repeat(3);
}

let input = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.';
console.log(bannerString(input, 30));
// Lorem ipsum dolor sit amet,...
console.log(bannerString(input, 60));
// Lorem ipsum dolor sit amet, consectetur adipiscing elit....
console.log(bannerString(input, 90));
// Lorem ipsum dolor sit amet, consectetur adipiscing elit....
```

## String.prototype.trimStart() & String.prototype.trimEnd() & String.prototype.trim()
先頭／末尾／両端の改行・空白文字を削除する。
```jsx
// Syntax
str.trimStart() // start from left
str.trimEnd() // end up right
str.trim() // trim head and tail
```
```jsx
let str = '     Lorem  123 ';
console.log(str.length);
// 16
console.log(str.trimStart(), str.trimStart().length);
// Lorem  123  11
console.log(str.trimEnd(), str.trimEnd().length);
//      Lorem  123 15
console.log(str.trim(), str.trim().length);
// Lorem  123 10
```
```jsx
let str = '\t\f\r\n\v\u0020\u00a0\u3000\u2028\u2029Lorem\t\f\r\n\v\u0020\u00a0\u3000\u2028\u2029';
console.log(str.length);
// 25
console.log(str.trimStart().length);
// 15
console.log(str.trimEnd().length);
// 15
console.log(str.trim().length);
// 5
```
改行・空白文字について前の文章では少しまとめました。

https://qiita.com/hu-yu/items/c381e9c191c7a7400671#%E5%88%B6%E5%BE%A1%E6%96%87%E5%AD%97


# Iterator
普通のforループでは`Code Unit`単位で文字列を処理するので、サロゲートペアなど二つの`Code Unit`の組み合わせや、`0xffff`を超えたUnicodeを正しく処理できません。
ここで`Code Point`単位で文字列を識別する`for...of`と`Array.from()`をしたいと思います。（ほかにメソッドが分かればまた更新します。）

https://jsprimer.net/basic/string-unicode/#loop-each-code-point

## for...of
```jsx
let str = '🍎🍇🍎🥕🍒';
for (let item of str) {
  console.log(item.codePointAt(0));
}
// 127822
// 127815
// 127822
// 129365
// 127826
```

## Array.from()
```jsx
function countCodePoints(str, target) {
  let count = 0;
  Array.from(str, (x) => {
    if (x === target) count += 1;
  })
  return count;
}
console.log(countCodePoints(str, '🍎')); // 2
```

# JSON
データ交換のための通用フォーマットオブジェクト。（自分の解釈です。実際はもっと複雑。）
`JSON`文字列はオブジェクト、配列、数値、文字列、論理値そして`null`を直列化（serialized）／整列化（marshalled）されるオブジェクトと呼ばれる。

## Methods
`JSON.stringify()`は引数を`JSON`フォーマットのテキスト（String）に転換し、
`JSON.parse()`は`JSON`テキストからJavaScriptが対応する値に転換する。
```jsx
// Syntax
JSON.stringify(value[, replacer[, space]])
// note
value: Object, Array, Number, String, boolean, null
replacer: Array of properties / function replacer(key, value)
space: space characters for formatting (space <= 10)

JSON.parse(text, reviver)
// note
text: JSON-string
reviver: function replacer(key, value)
```
- `NaN`、 `Infinity`など文字列できない数値は強制的に`null`に転換される。
- 関数プロパティ、シンボルキーと値、`undefined`を格納しているプロパティはスキップされる。

### JSON.stringify() - value
```jsx
let jsonArr = JSON.stringify(['1', 2, 'true', false, null, NaN, Infinity]);
console.log(jsonArr);
// ["1",2,"true",false,null,null,null]
console.log(JSON.parse(jsonArr));
// [
//   '1', 2,
//   'true', false,
//   null, null,
//   null
// ]
```
```jsx
let jsonObj = JSON.stringify({
  a: '1',
  b: 2,
  c: 'true',
  d: false,
  e: null,
  f: NaN,
  g: Infinity,
  h: {
    a: '5'
  },
  i: [4, 5, 6],
  myMethod() {
    console.log('myMethod');
  },
  [Symbol('id')]: 123,
  j: Symbol('foo'),
  k: undefined
})
console.log(jsonObj);
// {"a":"1","b":2,"c":"true","d":false,"e":null,"f":null,"g":null,"h":{"a":"5"},"i":[4, 5, 6]}
console.log(JSON.parse(jsonObj));
/*
{
  a: '1',
  b: 2,
  c: 'true',
  d: false,
  e: null,
  f: null,
  g: null,
  h: { a: '5' },
  i: [ 4, 5, 6 ]
}
*/
```

`JSON.stringify()`はオブジェクトラッパー関数から返したオブジェクトでも`JSON`文字列に転換できる。`JSON`文字列にしたときラッパーが捨てられ、再び`JSON.parse()`で転換すると対応する値になります。
```jsx
// object wrapper => json => corresponding value
// console.log(typeof (new Number(5)));// object
let num = JSON.stringify(new Number(5));
console.log(num);
// 5 // string
console.log(JSON.parse(num));
// 5 // number

// console.log(typeof (new String('abc'))); // object
let str = JSON.stringify(new String('abc'));
console.log(str);
// "abc" // string with double quotes
console.log(JSON.parse(str));
// abc // string
```

### JSON.stringify() - replacer
循環構造（circular structure）／循環参照（circular references）のオブジェクトをそのまま`JSON`に転換したらエラーが出てきます。

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#exceptions

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#issue_with_serializing_circular_references

```jsx
let room = {
  number: 10
}
let guests = {
  title: 'Party',
  participants: ['Taro', 'Jiro']
};
guests.place = room;
room.occupiedBy = guests;
console.log(guests.place, room.occupiedBy);
/*
<ref *1> {
  number: 10,
  occupiedBy: {
    title: 'Party',
    participants: [ 'Taro', 'Jiro' ],
    place: [Circular *1]
  }
} <ref *1> {
  title: 'Party',
  participants: [ 'Taro', 'Jiro' ],
  place: { number: 10, occupiedBy: [Circular *1] }
}
*/
console.log(JSON.stringify(guests));
// TypeError: Converting circular structure to JSON
```
`JSON.stringify`の第2引数に循環参照を避けたプロパティの配列を渡したら問題なく`JSON`にしてくれます（ネストされたプロパティも適用される）。
```jsx
let room = {
  number: 10
};
let guests = {
  title: 'Party',
  participants: [{ name: 'Taro' }, { name: 'Jiro' }],
  place: room
};
room.occupiedBy = guests;

console.log(JSON.stringify(guests, ['title', 'participants']));
// {"title":"Party","participants":[{},{}]}
console.log(JSON.stringify(guests, ['title', 'participants', 'name', 'place', 'number']));
// {"title":"Party","participants":[{"name":"Taro"},{"name":"Jiro"}],"place":{"number":10}}

console.log(JSON.stringify(guests, ['title', 'number', 'name', 'place', 'participants']));
// {"title":"Party","place":{"number":10},"participants":[{"name":"Taro"},{"name":"Jiro"}]}
```
**（第2引数に配列入れた`JSON`内部の順番は配列によって変わる。）**

循環参照のプロパティが衝突しない限りうまくいきますが。
```jsx
console.log(JSON.stringify(guests, ['occupiedBy']));
// {}
console.log(JSON.stringify(guests, ['title', 'occupiedBy']));
// {"title":"Party"}
console.log(JSON.stringify(guests, ['place', 'occupiedBy']));
// TypeError: Converting circular structure to JSON
```
そして循環参照を避けるには`undefined`で`JSON`に無視させましょう。
```jsx
console.log(JSON.stringify(guests, (key, value) => {
  return (key === 'occupiedBy') ? undefined : value;
}));
// {"title":"Party","participants":[{"name":"Taro"},{"name":"Jiro"}],"place":{"number":10}}
```
https://ja.javascript.info/json#ref-1015

こちらの例を参考にして書いたんですが。`replacer`の第2引数`value`を出力してよく見たら、順番でプロパティにアクセスし、ネストされた部分まで全部取り出してくれました。これで第1引数`key`に配列を入れて特定のプロパティだけ取り出してくれる、また、ネストのプロパティまで指定されないと無視されるのが何となくわかりました。

### JSON.stringify() - spaces
見やすさのため`JSON.stringify()`では第3引数に指定の数値(最大10まで)か、文字列（長さが10まで）を用いてインデントに使われます。
```jsx
let user = {
  name: 'Taro',
  age: 26,
  roles: {
    isAdmin: false,
    isEditor: true
  }
};
console.log(JSON.stringify(user));
// {"name":"Taro","age":26,"roles":{"isAdmin":false,"isEditor":true}}
console.log(JSON.stringify(user, null, 2));
/*
{
  "name": "Taro",
  "age": 26,
  "roles": {
    "isAdmin": false,
    "isEditor": true
  }
}
*/
```

### toJSON()
オブジェクトの`toString()`のような位置づけです。`JSON.stringify()`利用すると呼び出されます。
```jsx
let room = {
  number: 23,
};
let meetup = {
  title: "Conference",
  room
};
console.log(JSON.stringify(room)); // {"number":23}
console.log(JSON.stringify(meetup)); // {"title":"Conference","room":{"number":23}}

let room = {
  number: 23,
  toJSON() {
    return this.number;
  }
};
let meetup = {
  title: "Conference",
  room
};
console.log(JSON.stringify(room)); // 23
console.log(JSON.stringify(meetup)); // {"title":"Conference","room":23}
```

### JSON.parse() - str
`JSON`文字列です。
```jsx
× '{name: "Taro"}'
○ '{"name": "Taro"}'
× '{"surname": 'Yamada'}'
○ '{"surname": "Yamada"}'
× '{'isAdmin': false}'
○ '{"isAdmin": false}'
× '{"birthday": new Date(1995, 6, 6)}'
○ '{"birthday":"1995-07-05T16:00:00.000Z"}'

× '{"friends": "[0, 1, 2, 3]"}' // value is string
○ '{"friends": [0, 1, 2, 3]}'
× '{"family": "{"father": "...", "mother": "..."}"}'
○ '{"family": {"father": "...", "mother": "..."}}'
```

### JSON.parse() - reviver
`reviver()`は`(key, value)`二つのパラメータを持つ関数です。
`reviver()`で`JSON`文字列に操作することができます。
```jsx
let str = '{"name": "Taro", "birthday": "1995-07-05T16:00:00.000Z"}';
let person = JSON.parse(str, (key, value) => {
  if (key === 'birthday') return new Date(value);
  return value;
});
console.log(`${person.name}'s birthday is ${person.birthday.toDateString()}`);
// Taro's birthday is Thu Jul 06 1995
```
`replacer()`と同じく`reviver()`もネストされたプロパティへ動作します。

### Practice
こちらの例を書いてみました。

https://javascript.info/json#exclude-backreferences
```jsx
let room = {
  number: 10
};
let guests = {
  title: 'Party',
  participants: [{ name: 'Taro' }, { name: 'Jiro' }],
  place: room
};
room.occupiedBy = guests;
guests.self = guests;

console.log(JSON.stringify(guests, (key, value) => {
  // console.log(key)
  return (key !== '' && value === guests) ? undefined : value;
}))
// {"title":"Party","participants":[{"name":"Taro"},{"name":"Jiro"}],"place":{"number":10}}
```
下は`key`の出力結果です。
```

title
participants
0
name
1
name
place
number
occupiedBy
self
```
最初の`key`は`""`（`{"": guests}`）、そして順番でネストされたプロパティまで取り出しました。

## Newline/terminator in JSON
改行・空白文字がどう処理されるのか気になります。
`JSON`文字列のような構造（double quotes）包んだ改行・空白文字をパースしようとしたらエラーになる。
```jsx
console.log(JSON.parse('"foo"')); // foo 
console.log(JSON.parse('"\u0009"'));
// SyntaxError: Unexpected token    in JSON at position 1
```
プロパティバリューにすると問題なく処理してくれました。
```jsx
const json = JSON.stringify({
  horizontalTab: '\u0009',
  verticalTab: '\u000b',
  formFeed: '\u000c',
  carriageReturn: '\u000d',
  lineFeed: '\u000a',
  whiteSpace: '\u0020',
  nonBreakingSpace: '\u00a0',
  ideographicSpace: '\u3000',
  lineSeparator: '\u2028',
  paragraphSeparator: '\u2029',
  reverseSolidus: '\u005c'
});
console.log(json);
/*
{"horizontalTab":"\t","verticalTab":"\u000b","formFeed":"\f","carriageReturn":"\
r","lineFeed":"\n","whiteSpace":" ","nonBreakingSpace":" ","ideographicSpace":"
 ","lineSeparator":"","paragraphSeparator":"","reverseSolidus":"\\"}
*/
console.log(JSON.parse(json));
// {
//   horizontalTab: '\t',
//   verticalTab: '\x0B',
//   formFeed: '\f',
//   carriageReturn: '\r',
//   lineFeed: '\n',
//   whiteSpace: ' ',
//   nonBreakingSpace: ' ',
//   ideographicSpace: '　',
//   lineSeparator: '',
//   paragraphSeparator: '',
//   reverseSolidus: '\\'
// }
```
**（ES2019から`JSON`の文字列やプロパティに`\u2028`、`\u2029`を格納することができました。）**
文字列の両端でなければ改行・空白文字が普通の空白(space)として処理できるが、
両端に置かれたら`SyntaxError`になってしまいます。これは暗黙の文字列`''`でも、テンプレートリテラルでもそうです。
```jsx
let str = '"abc\u2028def\u2029ghi"';
console.log(JSON.parse(str));
// abc def ghi
str = `"abc\u2028def\u2029ghi"`
console.log(JSON.parse(str));
// abc def ghi

// newline/terminator at the head or tail
str = '"\u0009abc\u2028def\u2029ghi"';
console.log(JSON.parse(str));
// SyntaxError: Unexpected token    in JSON at position 1
str = '"abc\u2028def\u2029ghi\u000b"';
console.log(JSON.parse(str));
/*
SyntaxError: Unexpected token
                               in JSON at position 12
*/

str = `"\u0009abc\u2028def\u2029ghi"`
console.log(JSON.parse(str));
// SyntaxError: Unexpected token    in JSON at position 1
str = `"abc\u2028def\u2029ghi\u000b"`
console.log(JSON.parse(str));
/*
SyntaxError: Unexpected token
                               in JSON at position 12
*/
```

ユーザからもらった`String`は改行・空白文字が混じっている状態かもしれないので、`JSON`にする前に両端の空白文字を切り捨てるメソッド`trim()`、または`replace()`正規表現で検索して書き換える方法を書いてみました。
```jsx
let json = '"\u2028abc\u2028def\u2029ghi\u2029"';

// trim()
function trimmedJsonString(json) {
  let str = json.slice(1, json.length - 1);
  return `"${str.trim()}"`;
}
console.log(JSON.parse(trimmedJsonString(json)));
// abc def ghi

// replace()
function replacedJsonString(json) {
  let str = json.slice(1, json.length - 1);
  return `"${str.replace(/^\s+|\s+$/, '')}"`;
}
console.log(JSON.parse(replacedJsonString(json)));
// abc def ghi

function replacedBySpace(json) {
  let str = json.slice(1, json.length - 1);
  return `"${str.replace(/\s+/g, ' ')}"`;
}
console.log(JSON.parse(replacedBySpace(json)));
//  abc def ghi
```
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#json.stringify_%E3%82%92%E3%81%9D%E3%81%AE%E3%81%BE%E3%81%BE_javascript_%E3%81%A8%E3%81%97%E3%81%A6%E4%BD%BF%E7%94%A8%E3%81%99%E3%82%8B%E9%9A%9B%E3%81%AE%E5%95%8F%E9%A1%8C

それぞれの改行・空白文字を保留したいときもあるかもしれませんが。
```jsx
let json = '"\u2028\u2028abc\u2028def\u2029ghi\u2029\u2029"';
function escapedJsonString(json) {
  let str = json.slice(1, json.length - 1);
  return `"${str.
    replace(/\u2028/g, '\\u2028').
    replace(/\u2029/g, '\\u2029')}"
    `;
}
console.log(JSON.parse(escapedJsonString(json)));
//   abc def ghi
```

## unicode in JSON
データ交換のためにスタンダードの`JSON`データはUTF-8でエンコードされなければならない。たとえば`U+D800`-`U+DFFF`ではサロゲートペアではない単独のコードポイントや、有効ではないペア（対応する文字が存在しない）はUTF-8のスタンダードに合わないです。
今のJavaScrip`JSON.stringify()`はそれらをエスケープ無しでそのまま文字列として出力し、さらなる処理はしません。
```jsx
// well-formed JSON.stringify
console.log(JSON.stringify('\ud834')); // "\ud834"
console.log(JSON.stringify('\udf06\ud834')); // "\udf06\ud834"
console.log(JSON.stringify('\ud834\udf06')); // "𝌆"
console.log(JSON.parse(JSON.stringify('\ud834\udf06'))); // 𝌆

console.log(JSON.stringify('\ud834') === '"\\ud834"'); // true
console.log(JSON.stringify('\udf06') === '"\\udf06"'); // true
```
- サロゲートペア：`U+10000`-`U+10FFFF`
- 上位サロゲート：`U+D800`-`U+DBFF`
- 下位サロゲート：`U+DC00`-`U+DFFF`
```jsx
let str = '\ud834\udf06\ud834abc\ud834\udf06';
console.log(JSON.stringify(str, (key, value) => {
  return value.replace(/[\ud800-\udfff]/g, '')
}));
// "abc"
```
（思ったことのメモです。）
正規表現でサロゲート排除するって本当にいいのかな？有効なサロゲートペアならちゃんと転換出来てるし、特殊の符号文字も場合によって存在する必要があると思うけれど、なかなか正規表現で取り出すことができなかった。

コードポイントが問題です。正規表現では下位上位という誤った組み合わせを摘出して処理するのができるが、連なるコードポイント`\ud834\udf06\ud834`では`\ud834\udf06`は正しい組み合わせに対して`\udf06\ud834`は間違ったので排除すると、`\ud834`はペアになれなくてバグになるしかありません。

なので一度サロゲートへ処理するのに必要だと感じました。
```jsx
// surrogate pair in string
let str = '\ud834\udf06\ud834abc\ud834\udf06';
function findSurrogatePair(str) {
  return Array.from(str, (item) => {
    return (item.codePointAt() > 65535) ?
      `0x${item.codePointAt().toString(16)}/` : item
  }).join('');
}

// JSON.stringify()
let json = JSON.stringify(str, (key, value) => {
  let fixedString = findSurrogatePair(value);
  return fixedString.replace(/[\ud800-\udfff]/g, '');
});
console.log(json);
// "0x1d306/abc0x1d306/"

// JSON.parse()
let jsonToString = JSON.parse(json, (key, value) => {
  return value.replace(/0x[a-f0-9]{5,6}\//g, (item) => {
    let str = item.slice(0, item.length - 1);
    return String.fromCodePoint(str);
  })
});
console.log(jsonToString);
// 𝌆abc𝌆

// surrogate pair in obj
let obj = {
  string1: '𝌆abc𝌆',
  string2: '\ud834\udf06\ud834abc\ud834\udf06'
}

let json = JSON.stringify(obj, (key, value) => {
  if (key !== '' && typeof value === 'string') {
    let fixedObj = findSurrogatePair(value);
    return fixedObj.replace(/[\ud800-\udfff]/g, '');
  }
  return value
});
console.log(json);
// {"string1":"0x1d306/abc0x1d306/","string2":"0x1d306/abc0x1d306/"}

let jsonToObj = JSON.parse(json, (key, value) => {
  if (key !== '' && typeof value === 'string') {
    return value.replace(/0x[a-f0-9]{5,6}\//g, (item) => {
      let str = item.slice(0, item.length - 1);
      return String.fromCodePoint(str);
    })
  }
  return value;
});
console.log(jsonToObj);
// {string1: '𝌆abc𝌆', string2: '𝌆abc𝌆'}
```
下はまとめて書き直した関数です。
```jsx
// surrogate pair in string
function surrogateStringToJSON(str) {
  let fixedString = Array.from(str, (item) => {
    return (item.codePointAt() > 65535) ?
      `0x${item.codePointAt().toString(16)}/` : item
  }).join('').replace(/[\ud800-\udfff]/g, '');
  return JSON.stringify(fixedString);
}
console.log(surrogateStringToJSON(str))
// "0x1d306/abc0x1d306/"

function surrogateJSONToString(json) {
  let fixedJson = json.replace(/0x[a-f0-9]{5,6}\//g, (item) => {
    let str = item.slice(0, item.length - 1);
    return String.fromCodePoint(str);
  })
  return JSON.parse(fixedJson);
}
console.log(surrogateJSONToString(surrogateStringToJSON(str)))
// 𝌆abc𝌆

// surrogate pair in obj
function surrogateObjToJSON(obj) {
  return JSON.stringify(obj, (key, value) => {
    if (key !== '' && typeof value === 'string') {
      return Array.from(value, (item) => {
        return (item.codePointAt() > 65535) ?
          `0x${item.codePointAt().toString(16)}/` : item
      }).join('').replace(/[\ud800-\udfff]/g, '');
    }
    return value
  })
}
console.log(surrogateObjToJSON(obj))

function surrogateJSONToObj(json) {
  return JSON.parse(json, (key, value) => {
    if (key !== '' && typeof value === 'string') {
      return value.replace(/0x[a-f0-9]{5,6}\//g, (item) => {
        let str = item.slice(0, item.length - 1);
        return String.fromCodePoint(str);
      })
    }
    return value;
  })
}
console.log(surrogateJSONToObj(surrogateObjToJSON(obj)))
```
物足りないと思いながら今の実力を出し切ったって感じがします。。
次回はテンプレートリテラルと、気になるところをまとめていきたいと思います。