ISO 8601 覚書と正規表現

ISO 8601 覚書と正規表現

日付の表現

  • 暦日付、年間通算日、週間日付の3種類だが、年間通算日と週間日付は除外する
暦日付
  • 書式は YYYY-MM-DD または YYYYMMDDを許容する
  • YYYYは4桁(0000-9999)、MMは2桁(01-12)、DDは2桁(01-31)とする
  • 下位省略表記については、YYYY-MM、YYYY を採用する
let REG_YYYYMMDD = /^([0-9]{4})(?:\-?(0[1-9]|1[0-2])(?:\-?(0[1-9]|[12][0-9]|3[01]))?)?$/;
  • 上位省略表記については、--MM-DD、--MM、---DDを許容する
let REG_YYYYMMDD = /^(?=(?:\d+(?:\-\d+(?:\-\d+)?)?|\-\-\d+?\-?\d+))([0-9]{4})?\D*(0[1-9]|1[0-2])?\D?(0[1-9]|[12][0-9]|3[01])?$/;
正規表現での考え方

前段階で2つの書式をチェックして、後半で数字をチェックする。
reg_exp.exec (string) => Array (4) [*****, YYYY, MM, DD]

更に拡張して元号年月日を許容する

  • 文字列の先頭 "西暦|明治|大正|昭和|平成|令和|M|T|S|H|R" を識別する。西暦年は4桁、他の元号は2桁を許容する
  • 明治初期は陰暦なのでその年代の使用には注意を要する。
  • 漢字交じりの日付文字列に限り数値の前後の空白を許容する。
  • 元号年は2桁の、"明治99年1月1日"を許容する。
  • 上位省略表記については除外する
let REG_GYYYYMMDD = /^(?=(?:(?:[MmTtSsHhRr]?\d{1,2}|\d{4})(?:\-?\d+(?:\-?\d+)?)?|\s*(?:西暦\s*\d{4}|(?:明治|大正|昭和|平成|令和)\s*\d{1,2})\s*年\s*(?:\d+\s*月\s*(?:\d+\s*日\s*)?)?))\s*([^\d\s]+)?\s*([0-9]{1,4})\D*\s*(0?[1-9]|1[0-2])\D*\s*(0?[1-9]|[12][0-9]|3[01])\D*\s*$/;

console.log (REG_GYYYYMMDD.exec ("西暦 2021 年 3 月 1 日"));
//=> Array(5) ["西暦 2021 年 3 月 1 日", "西暦", "2021", "3", "1" ]

時刻の表現

  • 書式は HH:MM:SS、HHMMSS、HH:MM、HHMM、-MM:SS、-MMSS、--SS を許容する

ISO 8601 継続時間表記を正規表現で抜き出す

ISO 8601 日時の期間文字列から W, Y, M, D, h, m, s を抜き出す。

書式は、P[n]Y[n]M[n]DT[n]H[n]M[n]S
または、P[n]W

/^P(?=(?:\d+(?:\.\d+)?W|(?:\d+(?:\.\d+)?[YMD]){1,3}(?:T(?:\d+(?:\.\d+)?[HMS]){1,3})?|T(?:\d+(?:\.\d+)?[HMS]){1,3})$)(?:(.+)W|(?:(.+)Y)?(?:([\d\.]+)M)?(?:(.+)D)?)(?:T(?:(.+)H)?(?:(.+)M)?(?:(.+)S)?)?$/

Date オブジェクトと継続時間表記の文字列の加算

 function addDateDuration (date = new this, iso8601_duration_string, scale = 1) {
    const    //週の値は整数に制限[0-53], Y,M,D,h,m,s 共に少数を許容する
      REG_ISO8601_DURATION = /^P(?=(?:\d+(?:\.\d+)?W|(?:\d+(?:\.\d+)?[YMD]){1,3}(?:T(?:\d+(?:\.\d+)?[HMS]){1,3})?|T(?:\d+(?:\.\d+)?[HMS]){1,3})$)(?:(.+)W|(?:(.+)Y)?(?:([\d\.]+)M)?(?:(.+)D)?)(?:T(?:(.+)H)?(?:(.+)M)?(?:(.+)S)?)?$/,
      SECONDS = [ 86400, 3600, 60, 1 ],
      ONE_DAY = 365.2422 / 12,
      { floor: INT, ceil: CEIL } = Math;

    let parm = REG_ISO8601_DURATION.exec (iso8601_duration_string);
    if (parm) {
      let
        [, W, Y, M, D, h, m, s] = parm.map (n=> parseFloat (n) || 0),
        YM = Y * 12 + M,//Y,M の端数はDに加算して切り捨てる (時分秒に影響しない)
        ms = [D + W * 7 + INT ((YM % 1) * ONE_DAY), h, m, s].reduce ((a, b, i)=> a + b * SECONDS[i], 0) * 1000;
      date.setMonth (date.getMonth () + INT (YM * scale));
      date.setTime (date.getTime () + INT (ms * scale));
      return date;
    }
    else
      throw new Error ('Error!!: 解析できない文字列です。/:' + iso8601_duration_string);

  }

JavaScript エンターキーで移動する。その3

隠された要素は飛ばす。
[CTRL]+[Enter]では、 checked にする
テキストエリア内での処理を追加
IMEの未変換文字がある状態での Enter キーの対応
ルートの設定が個別にできること

<!DOCTYPE html>
<title></title>
<meta charset="utf-8">
<style>
</style>


<body>
<form id="F">
  <p>
    <input name="a" value="a">
    <input name="b" value="b" type="hidden">
    <input name="c" value="c">
    <span style="display:none;">
      <input name="d" value="d" id="d">

    </span>
  </p>
  <textarea name="j" cols="40" rows="10">fcvnruewihvnbre
vfreuoih\vreiojnreoi
vreiojnreoivreiojnreoi
  </textarea>
  <p>
    <input name="e" value="e">
    <input name="f" value="f" id="f">
  </p>
  <input type="reset">
  <p>
    <input type="radio" name="g" value="g0" checked>
    <input type="radio" name="g" value="g1">
    <input type="radio" name="g" value="g2">
  </p>

  <p>
    <input type="checkbox" name="h" value="h0" checked>
    <input type="checkbox" name="h" value="h1">
    <input type="checkbox" name="h" value="h2">
  </p>
  <p>
    <select name="i">
      <option value="i0">i0</option>
      <option value="i1">i1</option>
      <option value="i2">i2</option>
    </select>
  </p>
</form>


<script>

class ExEnter {

  constructor (root = document.documentElement, option = { }) {
    this.root = root;
    this.walker = document.createTreeWalker (root, NodeFilter.SHOW_ELEMENT, this.filter, true);
    this.option = Object.assign ({ }, this.constructor.defaultOption, option);

    //IME日本語入力中で未変換文字があり Enterが押されたことを感知するには
    //keypress イベントがが実行されないことを利用する
    this.imeFlag = null; //ime
  }


  //ルートの中の最初の要素を返す
  get firstElement () {
    this.walker.currentNode = this.root;
    return this.walker.firstChild ();
  }


  //ルートの最後の要素を返す
  get lastElement () {
    this.walker.currentNode = this.root;
    return this.walker.lastChild ();
  }


  //要素を移動する
  move (target, direction = false) {
    const
      walker = this.walker,
      isLoop = this.option.loop;

    this.walker.currentNode = target;
    let e = (direction)
    ? walker.previousNode () || (isLoop ? this.lastElement: null)
    : walker.nextNode ()     || (isLoop ? this.firstElement: null);
    e && e.focus ();
  }


  filter (node) {
    if (/^(INPUT|TEXTAREA|SELECT|BUTTON)$/.test (node.tagName))
      if (! node.disabled && ! node.readOnly) {
        if (! isHide (node)) 
          return NodeFilter.FILTER_ACCEPT;
      }
    return NodeFilter.FILTER_SKIP;
    //__
    function isHide (node) {
      for (let e = node, s; e; e = e.parentNode) {
        if (e === document.body) break;
        s = getComputedStyle (e, null);
        if ('none' === s.display
          || 'hidden' === s.visibility
          || !parseFloat (s.opacity || '') 
          || !parseInt (s.height || '', 10)
          || !parseInt (s.width || '', 10)
        ) return true;
      }
      return false;
    }
  }


  //_____

  //テキストエリアのキャレットの位置で分割(改行)する
  textareaSpliter (e) {
    let
      v = e.value,
      a = v.substring (0, e.selectionStart);

    e.value = a + '\n' + v.substr (e.selectionEnd);
    e.selectionStart = e.selectionEnd = a.length + 1;
  }

  
  //[CTRL]が押されていれば、適用させる
  apply (e, sw, prev) {
    let r = false; //要素に対して操作を行ったか
    switch (e.type) {
      case 'radio': case 'checkbox' :
        if (sw) {
          e.checked = !e.checked;
          r = this.option.stay;
        }
        break;
      case 'button' : case 'submit' : case 'reset' :
        if (sw) {
          let event = document.createEvent ('MouseEvents');
          event.initEvent("click", false, true);
          e.dispatchEvent (event);
          r = this.option.stay;
        }
        break;
      case 'textarea' :
        if ((sw ^ this.option.textarea_enter) && !prev) {
          this.textareaSpliter (e);
          r =true;
        }
        break;
    }
    return r;
  }


  //_____

  handleEvent (event) {
    this[event.type+ 'Handler'].call (this, event, event.target);
  }

  keyupHandler (event, e) {
    let { code, shiftKey, ctrlKey } = event;
    if ('Enter' !== code) return;
    if (! this.imeFlag) //IMEの動作で未変換中の文字があると判断してスルー
      return event.preventDefault ();
    this.imeFlag = null;
    if (! this.apply (e, ctrlKey, shiftKey))
      this.move (e, shiftKey);
  }

  keypressHandler (event, e) {
    if (/^(submit|reset|button|textarea)$/.test (e.type))
      event.preventDefault ();
    this.imeFlag = true;//未変換文字がある場合の対処として
  }

  //________

  static defaultOption = {
    stay: false, //true: radio,checkbox,submit,button を作用させた後に自動的に移動する
    textarea_enter: false, //true:テキストエリア内では改行する ,false:[Ctrl]+[Enter]で改行
    loop: true, //要素を巡回する
  };


  static create (root = document.documentElement, option = { }) {
    let obj = new this (root, option);
    root.addEventListener ('keyup', obj, false);
    root.addEventListener ('keypress', obj, false);
    return obj;
  }
}

ExEnter.create ();


</script>

JavaScriptでスライドを作る!しかもスクリプトは1行

基本的な考え方

  1. アニメーションはスタイルシートで行い。対象の要素群の先頭にだけ適用する。
  2. アニメーションが終了するとイベントが発生するのでそれを監視する。
  3. イベントの終了した要素を、その要素群の最後尾に移動する。

寺尾での回答

https://teratail.com/questions/321270

<style>
div {
  width: 800px;
  height: 200px;
  overflow: hidden;
  margin: 0 auto;
}

ul {
  white-space: nowrap;
  list-style: none;
  padding: 0;
  margin: 0;
}
ul li {
  display: inline-block;
  width: 400px;
  height: 200px;
  margin: 0;
  padding: 0;
}

li:first-of-type {
  animation-name: expansion;
  animation-duration: 2s;
  animation-timing-function:ease;
  animation-delay: 2s;
  animation-iteration-count: 1;
  animation-direction: normal;
  animation-fill-mode: backwards;
  animation-play-state: running;  
}

@keyframes expansion{
  0% { margin-left:-200px; }
  100% { margin-left:-600px; }
}

</style>

<body>
<div>
  <ul>
    <li><img src="https://placehold.jp/400x200.png" alt="">
    <li><img src="http://placehold.jp/24/cc9999/993333/400x200.png" alt="">
    <li><img src="https://placehold.jp/400x200.png" alt="">
    <li><img src="http://placehold.jp/24/cc9999/993333/400x200.png" alt="">
    <li>WHAT's NEW! ^^; WHAT's NEW! ^^; WHAT's NEW! ^^;
  </ul>
</div>

<script>
document.querySelector ('ul')
  .addEventListener ('animationend',
    ({target: e})=> e.parentNode.appendChild (e), false);
</script>

SELECT[multiple]:selected を取得する方法を発明した?

そもそも SELECT:selected なんてものはない!
そこで change イベントで select[multiple] をすべて監視し、selected になったもに
"data-selected" を付与しておけば、いつでも querySelectorAll で取得できる。
もちろん、クエリーにもそれを付与しなければならないが。

let select =
  document.querySelectorAll (
    'select:not([multiple]), select[multiple] option[data-selected]'
 );

考え方は、select[multiple]を除外してその子要素の option 要素を集める
option 要素の親要素をたどれば select 要素に行き着く。
そこで name を取得すればOK!


Memo:
select.selectedOptions があることを知った。

FORM要素(とは限らない)から、有効な値を取り出すには・・・

これで、 radio, checkbox, select[multiple] から有効な値を取得することができるようになった。

<!DOCTYPE html>
<meta charset="UTF-8" />
<title>Document</title>
<style>
</style>
<body>
<form>
  <p><input name="A" value="a">
  <p><input name="B" value="b" type="hidden">
  <p><output name="C" value="cccc">c</output>
  <p><textarea name="D">d</textarea>
  <p>
    <input name="E" value="e0" type="radio">E0 /
    <input name="E" value="e1" type="radio" checked>E1 /
    <input name="E" value="e2" type="radio">E2
  <p>
    <input name="F" value="f0" type="checkbox" checked>F0 /
    <input name="F" value="f1" type="checkbox">F1 /
    <input name="F" value="f2" type="checkbox" checked>F2
  <p>
    <select name="G">
      <option value="g0">G0</option>
      <option value="g1" selected>G1</option>
      <option value="g2">G2</option>
    </select>
  </p>
  <p>
    <select name="H" multiple id="H">
      <option value="h0" selected>H0</option>
      <option value="h1">H1</option>
      <option value="h2" selected>H2</option>
    </select>
  </p>

  <input type="button" value="Test" onclick="test()">
</form>
<div>
  <textarea id="T" cols="60" rows="30"></textarea>
</div>

<script>

class SurveillanceSELECT {
  constructor (root = document, key) {
    this.root = root;
    this.key = 'data-' + key;
  }

  switch (select) {
    for (let op of select.options) {
     op.selected 
     ? op.setAttribute (this.key, '')
     : op.removeAttribute (this.key);
    }
  }

  setting () {
    Array.from (
      this.root.querySelectorAll ('select[multiple]'),
      sel => this.switch (sel)
    );
  }

  handleEvent (event) {
    let e = event.target;
    if ('select-multiple' === e.type)
      this.switch (e);
  }

  static create (root = document) {
    const key = 'selected';
    let obj = new this (root, key);
    obj.setting ();
    root.addEventListener ('change', obj, false);
    return obj;
  }
}

SurveillanceSELECT.create ();
//_________________________________


function test () {
  const q0 = '*[name]:not([type="checkbox"]):not([type="radio"]):not(select[multiple]),*[name]:checked,select[name][multiple] option[data-selected]';
  let es = document.querySelectorAll (q0);
  T.value = [...es].map(e=>[(e.nodeName == 'OPTION' ? e.parentNode: e).name, e.value].join (" : ")).join (" \n");
}

</script>