JavaScript 30-seconds-of-code String編
お仕事で若手エンジニアのみなさんから「エンジニアならブログを書きますよねぇ」という話になり、ネタ探しにGitHubのExploreを覗いていたら30-seconds-of-codeというリポジトリを見つけました。
https://github.com/30-seconds/30-seconds-of-code/tree/master/snippets
30秒で学べるJavaScriptのコードスニペット集って感じでしょうか。配列や文字列、関数オブジェクトなどいくつかのカテゴリーにコードがまとめらています。
- 🔌 Adapter
- 📚 Array
- 🌐 Browser
- ⏱️ Date
- 🎛️ Function
- ➗ Math
- 📦 Node
- 🗃️ Object
- 📜 String
- 📃 Type
- 🔧 Utility
まずは入りやすそうなStringを見てみました。
String
けっこうな数のコードがあります。いずれも関数として定義します。
- CSVToArray
- CSVToJSON
- README
- URLJoin
- byteSize
- capitalize
- capitalizeEveryWord
- compactWhitespace
- decapitalize
- escapeHTML
- escapeRegExp
- fromCamelCase
- indentString
- isAbsoluteURL
- isAnagram
- isLowerCase
- isUpperCase
- mapString
- mask
- pad
- palindrome
- pluralize
- removeNonASCII
- reverseString
- sortCharactersInString
- splitLines
- stringPermutations
- stripHTMLTags
- toCamelCase
- toKebabCase
- toSnakeCase
- toTitleCase
- truncateString
- unescapeHTML
- words
余談ですが、この辺の関数の名前から処理を想像できるのも大事ですよね。padとかmapStringとかtoKebabCaseとか。ケバブ。
中にはクイズみたいな面白いものもあります。たとえばpalindrome関数。palindromeは日本語だと"回文"という意味で、文字列を反対から読み上げても同じかどうか検証します。サンプルコードはこんなかんじです。
const palindrome = str => { const s = str.toLowerCase().replace(/[\W_]/g, ''); return s === [...s].reverse().join(''); };
palindrome('taco cat'); // true
String.prototypeにはreverse関数がないので、いったん文字の配列に変換してからreverseしてjoinで文字列として再結合しています。コードをよく見ると、
[...s]
おっとなにこれ、JS詳しくないマンだと拒否反応がでますが、...はスプレッドオペレータというもので文字列に対して使うとs.split("")と同様の結果となります。
s = "taco cat" "taco cat" s.split("") (8) ["t", "a", "c", "o", " ", "c", "a", "t"] [...s] (8) ["t", "a", "c", "o", " ", "c", "a", "t"]
他にもキャメルケース変換、スネークケース変換、ケバブケース変換(、、ケバブ?)など、面白い関数がたくさんあります。あと正規表現もバンバン出てくるので、真面目にやれば正規表現の勉強になると思います。あとプログラミング習い始めの人、配列とか関数とかオブジェクトとか理解していた人にはちょうど良いお題のように感じました。まずは無心になって写経するだけでも勉強になると思います。あと技術者のスキル測るのにも使えるんじゃない、、と思ったり。
というわけで原文の英語の方が読みやすい気もしますが、面白そうなので日本語にまとめてみました。
CSVToArray
CSV文字列を2次元配列に置き換えます。
第3引数のomitFirstRow
がtrue
の場合Array.prototype.slice()
とArray.prototype.indexOf('\n')
を使って先頭行を削除します。それからString.prototype.split('\n')
を使って個々の行を配列に変換します。第2引数のdelimiter
が省略された場合はデフォルトの区切り文字として,
を使います。また第3引数が省略された場合はCSV文字列の先頭行(タイトル行)を処理対象として含みます。
const CSVToArray = (data, delimiter = ',', omitFirstRow = false) => data .slice(omitFirstRow ? data.indexOf('\n') + 1 : 0) .split('\n') .map(v => v.split(delimiter));
CSVToArray('a,b\nc,d'); // [['a','b'],['c','d']]; CSVToArray('a;b\nc;d', ';'); // [['a','b'],['c','d']]; CSVToArray('col1,col2\na,b\nc,d', ',', true); // [['a','b'],['c','d']];
CSVToJSON
CSV文字列をオブジェクトの2次元配列に置き換えます。文字列の1行目はタイトル行として処理します。
Array.prototype.slice()
、Array.prototype.indexOf('\n')
、String.prototype.split(delimiter)
を使って1行目(タイトル行)を取得します。それからString.prototype.split('\n')
を使って行ごとの文字列配列を作り、Array.prototype.map()
とString.prototype.split(delimiter)
を使って、個々の行の中の値を取り出します。Array.prototype.reduce()
を使って個々の行の値を格納したオブジェクトを作ります。これらのオブジェクトはタイトル行をパースした際のキーを持ちます。第2引数を省略した場合は、デフォルトの区切り文字として,
を使います。
const CSVToJSON = (data, delimiter = ',') => { const titles = data.slice(0, data.indexOf('\n')).split(delimiter); return data .slice(data.indexOf('\n') + 1) .split('\n') .map(v => { const values = v.split(delimiter); return titles.reduce((obj, title, index) => ((obj[title] = values[index]), obj), {}); }); };
CSVToJSON('col1,col2\na,b\nc,d'); // [{'col1': 'a', 'col2': 'b'}, {'col1': 'c', 'col2': 'd'}]; CSVToJSON('col1;col2\na;b\nc;d', ';'); // [{'col1': 'a', 'col2': 'b'}, {'col1': 'c', 'col2': 'd'}];
URLJoin
URLのブロックを結合して、妥当なURLに置き換えます。
String.prototype.join('/')
を使ってURLのブロックを結合し、一連のString.prototype.replace()
と様々な正規表現使って妥当なURL(ダブルスラッシュの排除、プロトコルの適切なスラッシュの追加、パラメータの前のスラッシュの削除、2つ目以降のパラメータを&による結合)に置き換えます。
const URLJoin = (...args) => args .join('/') .replace(/[\/]+/g, '/') .replace(/^(.+):\//, '$1://') .replace(/^file:/, 'file:/') .replace(/\/(\?|&|#[^!])/g, '$1') .replace(/\?/g, '&') .replace('&', '?');
URLJoin('http://www.google.com', 'a', '/b/cd', '?foo=123', '?bar=foo'); // 'http://www.google.com/a/b/cd?foo=123&bar=foo'
byteSize
文字列の長さをbytesで返します。
文字列をBlobオブジェクトに変換してsizeプロパティを参照します。
const byteSize = str => new Blob([str]).size;
byteSize('😀'); // 4 byteSize('Hello World'); // 11
capitalize
文字列の先頭文字を大文字に変換します。
配列の分割代入とString.prototype.toUpperCase()
を使って先頭の文字を大文字にします。 ...restには先頭文字を除く文字の配列が格納されるのでArray.prototype.join('')
を使って再び文字列に復元しています。引数のlowerRestが指定されなかった場合は残りの文字列(先頭文字を除く)をそのまま使います。lowerRestにtrueが指定された場合は残りの文字列を小文字に置き換えます。
const capitalize = ([first, ...rest], lowerRest = false) => first.toUpperCase() + (lowerRest ? rest.join('').toLowerCase() : rest.join(''));
capitalize('fooBar'); // 'FooBar' capitalize('fooBar', true); // 'Foobar'
capitalizeEveryWord
文字列内の単語ごとの先頭文字を大文字に変換します。
各単語の先頭文字をString.prototype.replace()
でマッチさせ、String.prototype.toUpperCase()
で大文字に変換します。
const capitalizeEveryWord = str => str.replace(/\b[a-z]/g, char => char.toUpperCase());
capitalizeEveryWord('hello world!'); // 'Hello World!'
compactWhitespace
連続するホワイトスペース文字(スペース、タブ、改ページ、改行)をホワイトスペース文字1文字に置き換えます。
正規表現とString.prototype.replace()
を使うことで、2文字以上のホワイトスペース文字を1文字に置き換えます。
const compactWhitespace = str => str.replace(/\s{2,}/g, ' ');
compactWhitespace('Lorem Ipsum'); // 'Lorem Ipsum' compactWhitespace('Lorem \n Ipsum'); // 'Lorem Ipsum'
decapitalize
文字列の先頭文字を小文字に変換します。
配列の分割代入とString.prototype.toLowerCase()
を使って先頭の文字を大文字にします。 ...restには先頭文字を除く文字の配列が格納されるのでArray.prototype.join('')
を使って再び文字列に復元しています。引数のupperRestが指定されなかった場合は残りの文字列(先頭文字を除く)をそのまま使います。upperRestにtrueが指定された場合は残りの文字列を大文字に置き換えます。
const decapitalize = ([first, ...rest], upperRest = false) => first.toLowerCase() + (upperRest ? rest.join('').toUpperCase() : rest.join(''));
decapitalize('FooBar'); // 'fooBar' decapitalize('FooBar', true); // 'fOOBAR'
escapeHTML
HTMLで利用可能な文字列(HTML特殊文字)にエスケープします。
String.prototype.replace()
と正規表現を使って、マッチした文字をエスケープします。このときコールバック関数を使ってディクショナリ(オブジェクト)によって管理された特殊文字に変換します。
const escapeHTML = str => str.replace( /[&<>'"]/g, tag => ({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' }[tag] || tag) );
escapeHTML('<a href="#">Me & you</a>'); // '<a href="#">Me & you</a>'
escapeRegExp
String.prototype.replace()
を使って特殊文字をエスケープします。
Use String.prototype.replace() to escape special characters.
const escapeRegExp = str => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); escapeRegExp('(test)'); // \\(test\\)
fromCamelCase
キャメルケースの文字列を変換します。
String.prototype.replace()
を使って、キャメルケースを単語の並びに変換します。単語の並びはアンダースコア、ハイフン、スペースなどで補完します。
const fromCamelCase = (str, separator = '_') => str .replace(/([a-z\d])([A-Z])/g, '$1' + separator + '$2') .replace(/([A-Z]+)([A-Z][a-z\d]+)/g, '$1' + separator + '$2') .toLowerCase();
fromCamelCase('someDatabaseFieldName', ' '); // 'some database field name' fromCamelCase('someLabelThatNeedsToBeCamelized', '-'); // 'some-label-that-needs-to-be-camelized' fromCamelCase('someJavascriptProperty', '_'); // 'some_javascript_property'
indentString
文字列内の各行をインデントします。
String.prototype.replace
と正規表現を使って、指定されたインデント幅分、各行の先頭に指定された文字を追加します。第3引数indent
が省略された場合デフォルトのインデント文字として' '(半角スペース)を使います。
const indentString = (str, count, indent = ' ') => str.replace(/^/gm, indent.repeat(count));
indentString('Lorem\nIpsum', 2); // ' Lorem\n Ipsum' indentString('Lorem\nIpsum', 2, '_'); // '__Lorem\n__Ipsum'
isAbsoluteURL
与えられた文字列が絶対URLの場合にtrue、そうでない場合にはfalseを返します。
正規表現を使って、与えられた文字列が絶対URLか検証します。
const isAbsoluteURL = str => /^[a-z][a-z0-9+.-]*:/.test(str);
isAbsoluteURL('https://google.com'); // true isAbsoluteURL('ftp://www.myserver.net'); // true isAbsoluteURL('/foo/bar'); // false
isAnagram
文字列が他の文字列(大文字小文字は無視し、スペース、句読点、特殊文字は除去した文字列)のアナグラム(文字配列の並び替え)であるか検証します。
String.prototype.toLowerCase()
と、適切な正規表現を適用したString.prototype.replace()
を使って、不要な文字を除去し、
String.prototype.split('')
、Array.prototype.sort()
、Array.prototype.join('')
を使ってノーマライズ(文字の並びを整理)するnormalize関数を定義します。引数の2つの文字列にnormalize関数を適用して、文字列の並びが等しいか検証します。
const isAnagram = (str1, str2) => { const normalize = str => str .toLowerCase() .replace(/[^a-z0-9]/gi, '') .split('') .sort() .join(''); return normalize(str1) === normalize(str2); };
isAnagram('iceman', 'cinema'); // true
isLowerCase
文字列が小文字か検証します。
String.prototype.toLowerCase()
を使って、与えられた文字列を小文字に変換し、元の文字列と等しいか比較します。
const isLowerCase = str => str === str.toLowerCase();
isLowerCase('abc'); // true isLowerCase('a3@$'); // true isLowerCase('Ab4'); // false
isUpperCase
文字列が大文字か検証します。
String.prototype.toUpperCase()
を使って、与えられた文字列を大文字に変換し、元の文字列と等しいか比較します。
Convert the given string to upper case, using String.prototype.toUpperCase() and compare it to the original.
const isUpperCase = str => str === str.toUpperCase();
isUpperCase('ABC'); // true isLowerCase('A3@$'); // true isLowerCase('aB4'); // false
mapString
文字列に含まれる個々の文字に対して、指定されたコールバック関数を適用して、新たな文字列を生成します。
String.prototype.split('')
とArray.prototype.map()
を使って、コールバック関数(引数のfn)を文字列内の個々の文字に対して適用します。それからArray.prototype.join('')
を使って文字配列を文字列として再結合します。コールバック関数は3つの引数(現在の文字、現在の文字のインデックス、mapString関数の引数に指定された文字列)を受け取ります。
const mapString = (str, fn) => str .split('') .map((c, i) => fn(c, i, str)) .join('');
mapString('lorem ipsum', c => c.toUpperCase()); // 'LOREM IPSUM'
mask
文字列を指定されたマスク文字で置き換えます。ただし、文字列の後部については、引数のnumに指定された文字数分はマスクしません。
String.prototype.slice()
を使って、アンマスク(マスクしない)する文字列を取り出し、String.prototype.padStart()
で、元の文字列の長さ分のマスク文字を追加します。第2引数のnumが省略された場合、デフォルトで4文字のアンマスク文字を確保します。またnumに負の値が指定された場合は、文字列の先頭部分をアンマスクします。第3引数のmaskが省略された場合、デフォルトのマスク文字に*
を使います。
const mask = (cc, num = 4, mask = '*') => `${cc}`.slice(-num).padStart(`${cc}`.length, mask);
mask(1234567890); // '******7890' mask(1234567890, 3); // '*******890' mask(1234567890, -4, '$'); // '$$$$567890'
pad
対象の文字列が、指定された文字数を下回る場合に、文字列の両側に指定された文字を連結します。
String.prototype.padStart()
とString.protorype.padEnd()
を使って与えられた文字列の両側に文字を連結します。第3引数が省略された場合、半角スペースがデフォルトの文字として連結されます。
const pad = (str, length, char = ' ') => str.padStart((str.length + length) / 2, char).padEnd(length, char);
pad('cat', 8); // ' cat ' pad(String(42), 6, '0'); // '004200' pad('foobar', 3); // 'foobar'
palindrome
与えられた文字列が回文になっているか検証します。回文の場合、true、そうでない場合、falseを返します。
String.prototype.toLowerCase()
とString.prototype.replace()
を使って非アルファベット文字を除去して小文字に統一し、それからスプレッドオペレータ(...)を使って文字列を文字の配列に変換し、Array.prototype.reverse()
、String.prototype.join('')
を使って生成した文字列と、元の文字列(非アルファベット文字を除去して小文字に統一したもの)を比較します。
const palindrome = str => { const s = str.toLowerCase().replace(/[\W_]/g, ''); return s === [...s].reverse().join(''); };
palindrome('taco cat'); // true
pluralize
入力された数値によって、基準となる文字列を単数系、あるいは複数形にして返します。第1引数にオブジェクトが指定された場合、関数によって返却されるクロージャを返します。これは"s"による単純な複数形の変換だけでなく、指定されたディクショナリに含まれる複数形単語を返却します。
numが-1か1の場合、単数系の単語を返却します。numがそれ以外の値の場合、複数形の単語を返却します。第3引数のpluralが省略された場合、デフォルトで第2引数のwordに"s"を連結した文字列を複数形の単語として処理するので、必要に応じてカスタマイズすることができます。第1引数のvalがオブジェクトの場合、複数形の単語を格納したディクショナリを保持したクロージャを返却します。
const pluralize = (val, word, plural = word + 's') => { const _pluralize = (num, word, plural = word + 's') => [1, -1].includes(Number(num)) ? word : plural; if (typeof val === 'object') return (num, word) => _pluralize(num, word, val[word]); return _pluralize(val, word, plural); };
pluralize(0, 'apple'); // 'apples' pluralize(1, 'apple'); // 'apple' pluralize(2, 'apple'); // 'apples' pluralize(2, 'person', 'people'); // 'people' const PLURALS = { person: 'people', radius: 'radii' }; const autoPluralize = pluralize(PLURALS); autoPluralize(2, 'person'); // 'people'
removeNonASCII
印刷できないASCII文字を除去します。
正規表現を使って印刷できないASCII文字を除去します。
const removeNonASCII = str => str.replace(/[^\x20-\x7E]/g, '');
removeNonASCII('äÄçÇéÉêlorem-ipsumöÖÐþúÚ'); // 'lorem-ipsum'
reverseString
文字列を反転します。 Reverses a string.
スプレッドオペレータ(...)とArray.prototype.reverse()
を使って文字列内の文字の並びを逆順にしString.prototype.join('')
によって文字を連結して文字列とします。
const reverseString = str => [...str].reverse().join('');
reverseString('foobar'); // 'raboof'
sortCharactersInString
文字列内の文字をアルファベット順にソートします。
スプレッドオペレータ(...)とArray.prototype.sort()
、String.prototype.localeCompare()
を使って文字列内の文字をソートし、String.prototype.join('')
を使って再結合します。
const sortCharactersInString = str => [...str].sort((a, b) => a.localeCompare(b)).join('');
sortCharactersInString('cabbage'); // 'aabbceg'
splitLines
複数行の文字列を行の配列に分割します。
String.prototype.split()
と正規表現を使って行を検出し、配列を生成します。
const splitLines = str => str.split(/\r?\n/);
splitLines('This\nis a\nmultiline\nstring.\n'); // ['This', 'is a', 'multiline', 'string.' , '']
stringPermutations
⚠️ 警告:この関数は文字数が増えると処理時間が指数関数的に増加します。8-10文字を超えると、ブラウザはすべての異なる組み合わせを試みるのでハングするでしょう。
文字列のすべての置換(並び替えた文字列)を生成します(重複を含みます)。
再帰を使います。与えられた文字列の各文字について、残りの文字すべての部分置換を生成します。Use Array.prototype.map()
を使って文字と部分置換を連結し、Array.prototype.reduce()
によって配列の中のすべての置換を連結します。処理が再帰しますが、文字列が2文字か1文字の場合をベースケースとしています。
const stringPermutations = str => { if (str.length <= 2) return str.length === 2 ? [str, str[1] + str[0]] : [str]; return str .split('') .reduce( (acc, letter, i) => acc.concat(stringPermutations(str.slice(0, i) + str.slice(i + 1)).map(val => letter + val)), [] ); };
stringPermutations('abc'); // ['abc','acb','bac','bca','cab','cba']
stripHTMLTags
文字列からHTML/XMLタグを除去します。
正規表現を使って、文字列からHTML/XMLタグを除去します。
const stripHTMLTags = str => str.replace(/<[^>]*>/g, '');
stripHTMLTags('<p><em>lorem</em> <strong>ipsum</strong></p>'); // 'lorem ipsum'
toCamelCase
文字列をキャメルケースに変換します。
正規表現を使って文字列を単語に分解し、個々の単語の先頭文字を大文字にして結合します。
const toCamelCase = str => { let s = str && str .match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g) .map(x => x.slice(0, 1).toUpperCase() + x.slice(1).toLowerCase()) .join(''); return s.slice(0, 1).toLowerCase() + s.slice(1); };
toCamelCase('some_database_field_name'); // 'someDatabaseFieldName' toCamelCase('Some label that needs to be camelized'); // 'someLabelThatNeedsToBeCamelized' toCamelCase('some-javascript-property'); // 'someJavascriptProperty' toCamelCase('some-mixed_string with spaces_underscores-and-hyphens'); // 'someMixedStringWithSpacesUnderscoresAndHyphens'
toKebabCase
文字列をケバブケースに変換します。
正規表現を使って文字列を単語に分解し、個々の単語を-(ハイフン)をセパレータとして結合します。
const toKebabCase = str => str && str .match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g) .map(x => x.toLowerCase()) .join('-');
toKebabCase('camelCase'); // 'camel-case' toKebabCase('some text'); // 'some-text' toKebabCase('some-mixed_string With spaces_underscores-and-hyphens'); // 'some-mixed-string-with-spaces-underscores-and-hyphens' toKebabCase('AllThe-small Things'); // "all-the-small-things" toKebabCase('IAmListeningToFMWhileLoadingDifferentURLOnMyBrowserAndAlsoEditingSomeXMLAndHTML'); // "i-am-listening-to-fm-while-loading-different-url-on-my-browser-and-also-editing-xml-and-html"
toSnakeCase
文字列をスネークケースに変換します。
正規表現を使って文字列を単語に分解し、個々の単語を_(アンダースコア)をセパレータとして結合します。
const toSnakeCase = str => str && str .match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g) .map(x => x.toLowerCase()) .join('_');
toSnakeCase('camelCase'); // 'camel_case' toSnakeCase('some text'); // 'some_text' toSnakeCase('some-mixed_string With spaces_underscores-and-hyphens'); // 'some_mixed_string_with_spaces_underscores_and_hyphens' toSnakeCase('AllThe-small Things'); // "all_the_smal_things" toSnakeCase('IAmListeningToFMWhileLoadingDifferentURLOnMyBrowserAndAlsoEditingSomeXMLAndHTML'); // "i_am_listening_to_fm_while_loading_different_url_on_my_browser_and_also_editing_some_xml_and_html"
toTitleCase
文字列をタイトルケースに変換します。
正規表現を使って文字列を単語に分解し、個々の単語の先頭文字を大文字に変換して半角スペースで結合します。
const toTitleCase = str => str .match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g) .map(x => x.charAt(0).toUpperCase() + x.slice(1)) .join(' ');
toTitleCase('some_database_field_name'); // 'Some Database Field Name' toTitleCase('Some label that needs to be title-cased'); // 'Some Label That Needs To Be Title Cased' toTitleCase('some-package-name'); // 'Some Package Name' toTitleCase('some-mixed_string with spaces_underscores-and-hyphens'); // 'Some Mixed String With Spaces Underscores And Hyphens'
truncateString
指定したサイズで文字列を切り詰めます。
文字列のサイズの上限を指定します。切り詰められた文字列の後部に'...'を連結して返します。
const truncateString = (str, num) => str.length > num ? str.slice(0, num > 3 ? num - 3 : num) + '...' : str;
truncateString('boomerang', 7); // 'boom...'
unescapeHTML
正規表現と
String.prototype.replace()
と正規表現を使って、マッチした文字をアンエスケープします。このときコールバック関数を使ってディクショナリ(オブジェクト)によって管理されたアンエスケープ文字に変換します。
const unescapeHTML = str => str.replace( /&|<|>|'|"/g, tag => ({ '&': '&', '<': '<', '>': '>', ''': "'", '"': '"' }[tag] || tag) );
unescapeHTML('<a href="#">Me & you</a>'); // '<a href="#">Me & you</a>'
words
文字列を単語の配列に変換します。
指定された正規表現パターン(デフォルトでは非アルファベット文字)とString.prototype.split()
を使って、単語文字列の配列に変換します。またArray.prototype.filter()
によって空の文字列を除去しています。第2引数が省略された場合はデフォルトの正規表現パターンを使います。
const words = (str, pattern = /[^a-zA-Z-]+/) => str.split(pattern).filter(Boolean);
words('I love javaScript!!'); // ["I", "love", "javaScript"] words('python, javaScript & coffee'); // ["python", "javaScript", "coffee"]
まとめ
久しぶりのブログ面白かったです。String以外の他のもやってみようと思いました。不定期でGitHubに追記していきます。