yucatio@システムエンジニア

趣味で作ったものいろいろ

JavaScript Ruby Pythonで文字列中に変数を展開できる機能の名前と書き方

こんにちは。JavaScriptRubyPythonが頭の中でごちゃごちゃになっているブログ主です。

JavaScriptRubyPythonの文字列中に変数を展開できる機能の名前と書き方が頭の中でごちゃごちゃです。 RubyなのにJavaScriptの記法で書いてしまい、なぜ思った通りに表示されないのか悩んだことも1度ではありません。そんな自分のためにそれぞれの言語での文字列中に変数を展開できる機能の名前と書き方をまとめました。

環境

動作を確認したバージョンです。

JavaScript

日本語名 テンプレート文字列
英語名 Template literals (Template strings)
コード例
a = 1
b = 2
// ${式} を使用する
str = `${a}たす${b}は${a + b}です。`
// 1たす2は3です。

Ruby

日本語名 式展開
英語名 Interpolation
コード例
a = 1
b = 2
# #{式} を使用する
str = "#{a}たす#{b}#{a + b}です。"
# 1たす2は3です。

Python

日本語名 フォーマット済み文字列リテラル(f-string)
英語名 Formatted string literals(f-string)
コード例
a = 1
b = 2
# f'{式}'
str1 = f'{a}たす{b}は{a + b}です。'
# 1たす2は3です。

JavaScript Ruby Pythonの配列やキーバリュー要素を展開したり多重代入したりする操作の名前と記号

こんにちは。JavaScriptRubyPythonが頭の中でごちゃごちゃになっているブログ主です。

JavaScriptRubyPythonの配列とキーバリューオブジェクトが頭の中でごちゃごちゃです。ググろうとしても、「あの、アスタリスクつける操作、なんだっけ」となり、やりたいことをググる以前に操作の名前をググる必要があり、その際記号だと検索に引っかかりづらいため、かなり時間を取られます。 そんな自分のために、配列とキーバリューオブジェクトの記号を使った展開や代入の操作をまとめました。

配列とキーバリューオブジェクトの各言語での呼び名

配列とキーバリューオブジェクトの各言語での呼び名です。 配列とは何か、キーバリューオブジェクトとは何かの定義はしないので、以下を見てなんとなく理解してください。

JavaScript Ruby Python
配列 配列(Array) 配列(Array) リスト(list)
キーバリュー ハッシュ(Hash) オブジェクト(Object) 辞書(dictionary)

環境

動作を確認したバージョンです。

配列とキーバリューオブジェクトの各言語での記号を使用した展開や代入の名前とサンプルコード

配列とキーバリューオブジェクトの、各言語での記号を使用した展開や代入の名前とサンプルコードです。 サンプルコードは代表的な使用法に限りました。名前が分かれば細かい部分は検索が可能と思いますので。 各言語、細かい違いがありますので、使用の際には公式ページをご覧ください。名称に公式ページへのリンクが張ってあります。

配列の要素を一度に複数の変数に代入する

JavaScript Ruby Python
日本語名 分割代入 多重代入 シーケンスのアンパック
英語名 Destructuring assignment Multiple assignment
Array Decomposition
Sequence unpacking
コード例
const arr = [1, 2]
const [a, b] = arr
// a=1, b=2
arr = [1, 2]
a, b = arr
# a=1, b=2
my_list = [1, 2]
a, b = my_list
# a=1, b=2
備考

キーバリューの要素を一度に複数の変数に代入する

JavaScript Ruby Python
日本語名 分割代入 (該当なし) (該当なし)
英語名 Destructuring assignment - -
コード例
const obj = {a: 1, b: 2}
const {a, b} = obj
// a=1, b=2
- -
備考 メソッドの引数でも使用できる。キーワード引数の代替になる。 引数を受け取る場面ではキーワード引数が利用可能 引数を受け取る場面ではキーワード引数が利用可能

配列を展開してメソッドの引数にする

JavaScript Ruby Python
日本語名 スプレッド構文 配列展開*1 アンパック
英語名 Spread syntax Splat operator Unpacking
コード例
const myFunction = (a, b) => {
  console.log(`a:${a}, b:${b}`)
}
const arr = [1, 2]
myFunction(...arr)
// a:1, b:2
def my_function(a, b)
  puts "a:#{a}, b:#{b}"
end
arr = [1, 2]
my_function(*arr)
# a:1, b:2
def my_function(a, b):
    print(f'a:{a}, b:{b}')
my_list = [1, 2]
my_function(*my_list)
# a:1, b:2
備考 配列展開に使用される"*"をsplat operatorという 配列展開に使用される"*"をstar operatorという

オブジェクトを展開してメソッドの引数にする(キーワード引数)

JavaScript Ruby Python
日本語名 (該当なし) (不明)*2
(ハッシュ展開*3 )
アンパック
英語名 - Double splat operator Unpacking
コード例 -
def my_method(a:, b:)
  puts "a:#{a}, b:#{b}"
end
my_hash = { a: 1, b: 2 }
my_method(**my_hash)
# a:1, b:2
def my_function(*, a, b):
    print(f'a:{a}, b:{b}')
my_dict = {'a': 1, 'b': 2}
my_function(**my_dict)
# a:1, b:1
備考 分割代入を使用して、キーワード引数に似た機能を使用できる
const myFunction = ({a, b}) => {
  console.log(`a:${a}, b:${b}`)
}
const obj = {a: 1, b: 2}
myFunction(obj)
// a:1, b:2
"**"をつけなくても動くが、つけるのが推奨されている。

配列を展開して配列を結合する(ときに記号を使った書き方)

いずれも2つの配列を結合して新たな配列を作ります。もとの配列は変化しません。

JavaScript Ruby Python
日本語名 スプレッド構文 Array#+ 加算演算子
英語名 Spread syntax Array#+ addition operator
コード例
arr1 = [1, 2]
arr2 = [3, 4]
arr3 = [...arr1, ...arr2]
// [1, 2, 3, 4]
arr1 = [1, 2]
arr2 = [3, 4]
arr3 = arr1 + arr2
# [1, 2, 3, 4]
list1 = [1, 2]
list2 = [3, 4]
list3 = list1 + list2
# [1, 2, 3, 4]
備考

キーバリューを展開してキーバリューを結合する(ときに記号を使った書き方)

いずれも2つのキーバリューオブジェクトを結合して新たなキーバリューオブジェクトを作ります。もとのキーバリューオブジェクトは変化しません。

JavaScript Ruby Python
日本語名 スプレッド構文 (不明)*4
(ハッシュ展開*5 )
アンパック
英語名 Spread syntax Double splat operator Unpacking
コード例
obj1 = {a: 1, b: 2}
obj2 = {c: 3, d: 4}
obj3 = {...obj1, ...obj2}
// {a: 1, b: 2, c: 3, d: 4}
hash1 = {a: 1, b: 2}
hash2 = {c: 3, d: 4}
hash3 = {**hash1, **hash2}
# {:a=>1, :b=>2, :c=>3, :d=>4}
dict1 = {'a': 1, 'b': 2}
dict2 = {'c': 3, 'd': 4}
dict3 = {**dict1, **dict2}
# {'a': 1, 'b': 2, 'c': 3, 'd': 4}
備考

あとがき

配列、キーバリュー周りに関しては、意外とRubyPythonの記法が似ていました。キーワード引数については、Pythonの記法をRubyでも使えるようにしたものだそうです。

配列もキーバリューのオブジェクトの名前も、それらに関する操作の名前ももっと言語間で統一してくれたらいいのにな、と思いました。 新しい言語を作る時には新しい名前を付けたくなるのかも。

*1:配列展開というのが正式名称ではなく、「引数の直前に * がついている場合、その引数の値が展開されて渡されます。」という記述から配列展開という言葉を使用しています

*2:日本語の公式ページに記載が見つけられませんでした。情報求めます

*3:配列展開の転用。個人ブログでこのように呼んでいるケースもあります

*4:同上

*5:同上

date-fnsを使用してカレンダー作成する(その4: Material-UIのmakeStylesとpropsを使用してスタイルを変更する)

前回は組み込みコンポーネントをMaterial-UIのコンポーネントに置き換え、簡単なスタイルを適用しました。 今回はuseStyleにプロパティを渡して、プロパティごとに表示を切り替えます。

★前回の記事

yucatio.hatenablog.com

今回の変更点

  • 日曜日と土曜日の日付をそれぞれ赤色、青色にする
  • 表示対象でない月の日付は薄い色にする
  • 今日の日付を目立たせる

f:id:yucatio:20191223081351p:plain

デモ : date-fns calendar

なお、ブログ主はデザインセンスがないので、その辺はご容赦ください。

プロパティの値によってスタイルを変化させる

今回のカレンダーでは、以下のように平日か土曜日か日曜日か、および表示対象の月かどうかで文字の色を変更します。

表示対象の月の日 前後の月の日
平日 グレー
土曜日 薄い青
日曜日 薄い赤

Adapting based on props のページを参考に、propsの値によってスタイルを変更します。

CalendarTableCellというカスタムコンポーネントを追加し、TableCellを置き換えます。 CalendarTableCellには、曜日を表すwdayと、表示対象の月かどうかを表すisTargetMonthを渡します。表示対象の月かを判定するには、date-fnsのisSameMonthを使用します。

CalendarTableCellでは、propsuseCalendarCellStylesに渡し、classesを得ます。

useCalendarCellStylescolorの値に関数を指定し、propsを受け取れるようにします。 colorに指定した関数の中では、propsからwdayisTargetMonthを受け取り、その値によって色を返します。

import React, { useState }  from 'react'
// 中略

// 追加ここから
import isSameMonth from 'date-fns/isSameMonth'

import blue from '@material-ui/core/colors/blue'
import red from '@material-ui/core/colors/red'
// 追加ここまで

// 中略

// 追加ここから
const useCalendarCellStyles = makeStyles(theme => ({
  calendarCell: {
    color: ({wday, isTargetMonth}) => {
      if(isTargetMonth) {
        switch(wday) {
          case 0: // Sunday
            return red[500]
          case 6: // Saturday
            return blue[500]
          default:
            return theme.palette.text.primary
        }
      } else {
        // previous or next month
        switch(wday) {
            case 0: // Sunday
            return red[200]
          case 6: // Saturday
            return blue[200]
          default:
            return theme.palette.text.secondary
        }
      }
    },
  },
}))

function CalendarTableCell(props) {
  const {wday, isTargetMonth, children, ...other} = props
  const classes = useCalendarCellStyles(props)
  return (<TableCell className={classes.calendarCell} {...other}>{children}</TableCell>)
}
// 追加ここまで

function App() {
  const [targetDate, setTargetDate] = useState(new Date())
  const classes = useStyles()
  const calendar = getCalendarArray(targetDate)

  return (
    <div>
      <CssBaseline />
      <Paper className={classes.paper}>
        <Grid container justify="space-between">
          {/* 中略 */}
        </Grid>

        <Typography variant="h4" align="center" className={classes.yearmonth}>{format(targetDate, 'y年M月')}</Typography>
        <Table>
          <TableHead>
            {/* 中略 */}
          </TableHead>
          <TableBody>
            {calendar.map((weekRow, rowNum) => (
              <TableRow key={rowNum}>
                {weekRow.map(date => (
                  {/* 変更ここから */}
                  <CalendarTableCell key={getDay(date)} wday={getDay(date)} isTargetMonth={isSameMonth(date, targetDate)} align="center">
                    {getDate(date)}
                  </CalendarTableCell>
                  {/* 変更ここまで */}
                ))}
              </TableRow>
            ))}
          </TableBody>
        </Table>
      </Paper>
    </div>
  )
}

export default App

実行結果です。表示対象の月とそれ以外、また曜日でも文字の色が変わりました。

f:id:yucatio:20191224125713p:plain

f:id:yucatio:20191224125822p:plain

今日の日付を目立たせる

最後に、今日の日付を目立たせます。今回はは背景色を変えます。 表示している日付が今日かどうかはdate-fnsのisSameDayを利用します。

import React, { useState }  from 'react'
// 中略

// 追加ここから
import isSameDay from 'date-fns/isSameDay'

import pink from '@material-ui/core/colors/pink'
// 追加ここまで

// 中略

const useCalendarCellStyles = makeStyles(theme => ({
  calendarCell: {
    color: ({wday, isTargetMonth}) => {
      // 中略
    },
    // 追加ここから
    backgroundColor: ({isToday}) =>
      isToday ? pink[50] : "transparent"
    // 追加ここまで
  },
}));

function CalendarTableCell(props) {
  // isTodayを追加
  const {wday, isTargetMonth, isToday, children, ...other} = props
  const classes = useCalendarCellStyles(props)
  return (<TableCell className={classes.calendarCell} {...other}>{children}</TableCell>)
}

function App() {
  const [targetDate, setTargetDate] = useState(new Date())
  const classes = useStyles()
  const calendar = getCalendarArray(targetDate)
  const today = new Date()  // 追加

  return (
    <div>
      <CssBaseline />
      <Paper className={classes.paper}>
        <Grid container justify="space-between">
          {/* 中略 */}
        </Grid>

        <Typography variant="h4" align="center" className={classes.yearmonth}>{format(targetDate, 'y年M月')}</Typography>
        <Table>
          <TableHead>
            {/* 中略 */}
          </TableHead>
          <TableBody>
            {calendar.map((weekRow, rowNum) => (
              <TableRow key={rowNum}>
                {weekRow.map(date => (
                  {/* isToday={isSameDay(date, today)} を追加 */}
                  <CalendarTableCell key={getDay(date)} wday={getDay(date)} isTargetMonth={isSameMonth(date, targetDate)} isToday={isSameDay(date, today)} align="center">
                    {getDate(date)}
                  </CalendarTableCell>
                ))}
              </TableRow>
            ))}
          </TableBody>
        </Table>
      </Paper>
    </div>
  )
}

export default App

実行結果です。今日の日付の背景色が変わりました。

f:id:yucatio:20191223081351p:plain

ソースコード

ここまでのソースコードです。

GitHub - yucatio/date-fns-calendar at 3bebfa7d00b30156676517c1be7879c08b0022ee

環境

  • npm: 6.12.1
  • react: 16.12.0
  • date-fns: 2.8.1
  • Material-UI: 4.8.0

あとがき

簡単なカレンダー機能なのでブログの1記事になるかと思いましたがまさかの4記事になりました。

date-fns, react-hooks, Material-UIのmakeStylesと比較的新しめの機能に触れるいい機会になりました。

date-fnsは今回カレンダーを作成するのに必要な機能がほぼ盛り込まれていて大変便利だと感じました。

Material-UIのカスタムスタイルの使用には、今までwithStylesを使用してきましたが、useStyleの方がすっきり書けて良いと思いました。

date-fnsを使用してカレンダー作成する(その3: Material-UIのコンポーネントを使用して見た目を整える)

前回までで基本的なカレンダーの機能を作成することができました。今回は見た目を整えて行きましょう。Material-UIを利用します。

★前回の記事

yucatio.hatenablog.com

今回の変更点

  • 余白を作成する
  • Material-UIのボタンを使用する
  • ボタンを左端、中央、右端に配置する
  • 年月の表示に見出しを適用する
  • Material-UIのテーブルを使用する

f:id:yucatio:20191223215238p:plain

デモ : date-fns calendar

なお、ブログ主はデザインセンスがないので、その辺はご容赦ください。

ライブラリのインストール

Material-UIのパッケージをインストールします。

yarn add @material-ui/core

コーディング

順にコードを追加していきましょう。

Paperと余白の追加

はじめに、CssBaselineを追加し、背景色をつけます。 Paperの上にテーブルを載せましょう。Paperには余白を設定します。

import React, { useState }  from 'react'
// 中略
import { makeStyles } from '@material-ui/core/styles'  // 追加
import CssBaseline from '@material-ui/core/CssBaseline'  // 追加
import Paper from '@material-ui/core/Paper'  // 追加

// 追加ここから
const useStyles = makeStyles(theme => ({
  paper: {
    margin: theme.spacing(5, 10),
    padding: theme.spacing(5, 5),
  },
}));
// 追加ここまで

// 中略

function App() {
  const [targetDate, setTargetDate] = useState(new Date())
  const classes = useStyles() // 追加
  const calendar = getCalendarArray(targetDate)

  return (
    <div>
      <CssBaseline />  {/* 追加 */}
      <Paper className={classes.paper}>  {/* 追加 */}
        <div>
          <button onClick={() => setTargetDate(subMonths(targetDate, 1))}>前の月</button>
          <button onClick={() => setTargetDate(new Date())}>今月</button>
          <button onClick={() => setTargetDate(addMonths(targetDate, 1))}>次の月</button>
        </div>

        {format(targetDate, 'y年M月')}
        <table>
          {/* 中略 */}
        </table>
      </Paper>  {/* 追加 */}
    </div>
  )
}

export default App

実行結果です。カレンダーがPaperコンポーネントの上に載り、余白が追加されました。

f:id:yucatio:20191223214549p:plain

ボタンのスタイルを整える

次に、ボタンをMaterial-UIのButtonコンポーネントに置き換えます。 ボタンをそれぞれ親コンポーネントの左端、中央、右端に配置するため、Gridコンポーネントを利用します。

Grid containerjustify="space-between"を指定することで、ボタンが右端左端と中央に置かれます。

import React, { useState }  from 'react'
// 中略
import Button from '@material-ui/core/Button' // 追加
import Grid from '@material-ui/core/Grid' // 追加

// 中略

function App() {
  const [targetDate, setTargetDate] = useState(new Date())
  const classes = useStyles()
  const calendar = getCalendarArray(targetDate)

  return (
    <div>
      <CssBaseline />
      <Paper className={classes.paper}>
        {/* 変更ここから */}
        <Grid container justify="space-between">
          <Grid item>
            <Button variant="outlined" onClick={() => setTargetDate(subMonths(targetDate, 1))}>前の月</Button>
          </Grid>
          <Grid item>
            <Button variant="outlined" onClick={() => setTargetDate(new Date())}>今月</Button>
          </Grid>
          <Grid item>
            <Button variant="outlined" onClick={() => setTargetDate(addMonths(targetDate, 1))}>次の月</Button>
          </Grid>
        </Grid>
        {/* 変更ここまで */}

        {format(targetDate, 'y年M月')}
        <table>
            {/* 中略 */}
        </table>
      </Paper>
    </div>
  )
}

export default App

実行結果です。Material-UIのボタンに変更されました。右端左端、中央にボタンが配置されています。

f:id:yucatio:20191223214708p:plain

年月の表示に見出しを適用する

"2019年12月"などと表示されている部分にスタイルを適用しましょう。 Typographyを使用します。そのままだと上下が窮屈な感じがしたので、少し空白を設けました。

import React, { useState }  from 'react'
// 中略
import Typography from '@material-ui/core/Typography'  // 追加

const useStyles = makeStyles(theme => ({
  paper: {
    margin: theme.spacing(5, 10),
    padding: theme.spacing(5, 5),
  },
  // 追加ここから
  yearmonth: {
    margin: theme.spacing(2, 0, 1, 0),
  },
  // 追加ここまで
}));

// 中略

function App() {
  const [targetDate, setTargetDate] = useState(new Date())
  const classes = useStyles()
  const calendar = getCalendarArray(targetDate)

  return (
    <div>
      <CssBaseline />
      <Paper className={classes.paper}>
        <Grid container justify="space-between">
          {/* 中略 */}
        </Grid>

        {/* 変更ここから */}
        <Typography variant="h4" align="center" className={classes.yearmonth}>{format(targetDate, 'y年M月')}</Typography>
        {/* 変更ここまで */}
        <table>
          {/* 中略 */}
        </table>
      </Paper>
    </div>
  )
}

export default App

実行結果です。文字が大きくなり、中央に表示されました。

f:id:yucatio:20191223214937p:plain

テーブルのスタイルを整える

htmlタグ(正確にいうと組み込みのコンポーネント)とMaterial-UIのコンポーネントの変換表です。 この表に従ってコードを書き直します。

htmlタグ Material-UI コンポーネント
table Table
thead TableHead
tbody TableBody
tr TableRow
th, td TableCell

テーブルのヘッダの文字色と背景色も変更しました。文字をセルの左右中央に配置されるように指定しました。

import React, { useState }  from 'react'
// 中略
// 追加ここから
import Table from '@material-ui/core/Table';
import TableBody from '@material-ui/core/TableBody'
import TableCell from '@material-ui/core/TableCell'
import TableHead from '@material-ui/core/TableHead'
import TableRow from '@material-ui/core/TableRow'
// 追加ここまで

const useStyles = makeStyles(theme => ({
  paper: {
    margin: theme.spacing(5, 10),
    padding: theme.spacing(5, 5),
  },
  yearmonth: {
    margin: theme.spacing(2, 0, 1, 0),
  },
  // 追加ここから
  tableHead: {
    color: theme.palette.secondary.contrastText,
    backgroundColor: theme.palette.secondary.light,
  },
  // 追加ここまで
}));

// 中略

function App() {
  const [targetDate, setTargetDate] = useState(new Date())
  const classes = useStyles()
  const calendar = getCalendarArray(targetDate)

  return (
    <div>
      <CssBaseline />
      <Paper className={classes.paper}>
        <Grid container justify="space-between">
          {/* 中略 */}
        </Grid>

        <Typography variant="h4" align="center" className={classes.yearmonth}>{format(targetDate, 'y年M月')}</Typography>
        {/* 変更ここから */}
        <Table>
          <TableHead>
            <TableRow>
              <TableCell align="center" classes={{head: classes.tableHead, }}>日</TableCell>
              <TableCell align="center" classes={{head: classes.tableHead, }}>月</TableCell>
              <TableCell align="center" classes={{head: classes.tableHead, }}>火</TableCell>
              <TableCell align="center" classes={{head: classes.tableHead, }}>水</TableCell>
              <TableCell align="center" classes={{head: classes.tableHead, }}>木</TableCell>
              <TableCell align="center" classes={{head: classes.tableHead, }}>金</TableCell>
              <TableCell align="center" classes={{head: classes.tableHead, }}>土</TableCell>
            </TableRow>
          </TableHead>
          <TableBody>
            {calendar.map((weekRow, rowNum) => (
              <TableRow key={rowNum}>
                {weekRow.map(date => (
                  <TableCell key={getDay(date)} align="center">{getDate(date)}</TableCell>
                ))}
              </TableRow>
            ))}
          </TableBody>
        </Table>
        {/* 変更ここまで */}
      </Paper>
    </div>
  )
}

export default App

実行結果です。Material-UIのテーブルが使用されました。

f:id:yucatio:20191223215238p:plain

以上でMaterial-UIのコンポーネントへの置き換えが完了しました。

ソースコード

ここまでのソースコードです。

GitHub - yucatio/date-fns-calendar at 3c6585622b48bb92b7ed9f71c2ecc8cab3f31b8e

環境

  • npm: 6.12.1
  • react: 16.12.0
  • date-fns: 2.8.1
  • Material-UI: 4.8.0

次回の記事

次回は日付の色を調整します。

yucatio.hatenablog.com

date-fnsを使用してカレンダー作成する(その2: React hooksを利用して月を移動する)

前回の記事の続きです。前の月や次の月に遷移できるようにします。Reactバージョン16.8.0から登場したReact hooksを使用します。

★前回の記事

yucatio.hatenablog.com

要件

  • カレンダーを表示したい

最終的にはこちらが出来上がります↓

f:id:yucatio:20191223081351p:plain

デモ : date-fns calendar

仕様

  • デフォルトで今月のカレンダーを表示する
  • 日曜日を行の一番始めにする
  • "次の月"のボタンが押されたら、次の月のカレンダーを表示する
  • "前の月"のボタンが押されたら、前の月のカレンダーを表示する
  • "今月"のボタンが押されたら、今月のカレンダーを表示する

f:id:yucatio:20191223133127p:plain

f:id:yucatio:20191223133113p:plain

f:id:yucatio:20191223133143p:plain

コード

次の月、前の月、今月のボタンを追加したコードです。

import React, { useState }  from 'react'  // useStateを追加
import format from 'date-fns/format'
import getDate from 'date-fns/getDate'
import getDay from 'date-fns/getDay'
import eachDayOfInterval from 'date-fns/eachDayOfInterval'
import endOfWeek from 'date-fns/endOfWeek'
import eachWeekOfInterval from 'date-fns/eachWeekOfInterval'
import addMonths from 'date-fns/addMonths'  // 追加
import subMonths from 'date-fns/subMonths'  // 追加
import startOfMonth from 'date-fns/startOfMonth'
import endOfMonth from 'date-fns/endOfMonth'

const getCalendarArray = date => {
  const sundays = eachWeekOfInterval({
    start: startOfMonth(date),
    end: endOfMonth(date)
  })
  return sundays.map(sunday =>
    eachDayOfInterval({start: sunday, end: endOfWeek(sunday)})
  )
}

function App() {
  const [targetDate, setTargetDate] = useState(new Date())  // 変更
  const calendar = getCalendarArray(targetDate)

  return (
    <div>
      {/* 追加ここから */}
      <div>
        <button onClick={() => setTargetDate(current => subMonths(current, 1))}>前の月</button>
        <button onClick={() => setTargetDate(new Date())}>今月</button>
        <button onClick={() => setTargetDate(current => addMonths(current, 1))}>次の月</button>
      </div>
      {/* 追加ここまで */}

      {format(targetDate, 'y年M月')}
      <table>
        <thead>
          <tr>
            <th>日</th><th>月</th><th>火</th><th>水</th><th>木</th><th>金</th><th>土</th>
          </tr>
        </thead>
        <tbody>
          {calendar.map((weekRow, rowNum) => (
            <tr key={rowNum}>
              {weekRow.map(date => (
                <td key={getDay(date)}>{getDate(date)}</td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  )
}

export default App

コードの解説

React hooksを使用します。表示する月に属する日付をtargetDateに格納します。初期値はnew Date() です。更新用の関数にはsetTargetDateという名前をつけます。

const [targetDate, setTargetDate] = useState(new Date()); 

次に、前の月、今月、次の月のボタンを表示します。

      <div>
        <button>前の月</button>
        <button>今月</button>
        <button>次の月</button>
      </div>

実行結果です↓

f:id:yucatio:20191223121610p:plain

ボタンが押された時のアクションを追加します。 "次の月"が押されたときは、targetDateに1ヶ月足します。 "前の月"が押されたときは、targetDateから1ヶ月引きます。 "今月"が押されたときは、現在時刻をtargetDateに設定します。

      <div>
        <button onClick={() => setTargetDate(subMonths(targetDate, 1))}>前の月</button>
        <button onClick={() => setTargetDate(new Date())}>今月</button>
        <button onClick={() => setTargetDate(addMonths(targetDate, 1))}>次の月</button>
      </div>

動作確認です。

"前の月"を押したときに次の月に遷移します。

f:id:yucatio:20191223133113p:plain

"次の月"を押したときに次の月に遷移します。

f:id:yucatio:20191223133127p:plain

"今月"を押した時に、今月のカレンダーが表示されます↓

f:id:yucatio:20191223133143p:plain

以上でReact hooksを利用して月を移動することができました。

ソースコード

ここまでのソースコードです。

GitHub - yucatio/date-fns-calendar at 87835db6805769c8d10cf56aebdc0e06e06638bd

環境

  • npm: 6.12.1
  • react: 16.12.0
  • date-fns: 2.8.1

次回の記事

次回はMaterial-UIを利用して見た目を整えます。

yucatio.hatenablog.com

date-fnsを使用してカレンダー作成する(その1: 今月のカレンダーを表示する)

簡単なカレンダーを作成します。date-fnsというJavaScriptの日時ライブラリを使用します。

f:id:yucatio:20191223081351p:plain

デモ : date-fns calendar

要件

  • 今月のカレンダーを表示したい

仕様

  • 今月のカレンダーを表示する
  • 日曜日を行の一番始めにする

今回はこのような表示まで完成させます↓

f:id:yucatio:20191223083802p:plain

事前準備

date-fnsをインストールします。

yarn add date-fns

コード

はじめに、カレンダーに表示するための日付を格納した配列を作成しましょう。 週ごとに日付をまとめます。

import eachDayOfInterval from 'date-fns/eachDayOfInterval'
import endOfWeek from 'date-fns/endOfWeek'
import eachWeekOfInterval from 'date-fns/eachWeekOfInterval'
import startOfMonth from 'date-fns/startOfMonth'
import endOfMonth from 'date-fns/endOfMonth'

const getCalendarArray = date => {
  const sundays = eachWeekOfInterval({
    start: startOfMonth(date),
    end: endOfMonth(date)
  })
  return sundays.map(sunday =>
    eachDayOfInterval({start: sunday, end: endOfWeek(sunday)})
  )
}

const calendar = getCalendarArray(new Date())
console.table(calendar)

実行結果です。ブラウザのコンソールに、カレンダーに表示する日付が表示されました。

f:id:yucatio:20191223083244p:plain

コードの解説

getCalendarArray関数の最初に eachWeekOfInterval を使用しています。

この関数は、startendを渡すと、その期間が含まれる週の始めの日(日曜日)を配列で返します。 例えば、 startに2019年11月1日、endに2019年11月30日を渡すと、

[
  2019年10月27日,
  2019年11月03日,
  2019年11月10日,
  2019年11月17日,
  2019年11月24日
]

を返します(実際にはdate型の配列です)。ちょうど、11月のカレンダーに表示される日曜日と一致しますね。

f:id:yucatio:20191223082532p:plain

この関数に、作成したい月の月初と月末を渡すことで、カレンダーに含まれる日曜日を過不足なく取得することができます。 月初と月末を取得するには、それぞれ startOfMonthendOfMonth を利用します。

日曜日の配列をsundaysに格納します。

ここまでで、以下のプログラムが出来上がりました。

  const sundays = eachWeekOfInterval({
    start: startOfMonth(date),
    end: endOfMonth(date)
  })

次に、日曜日の日付から、その週に含まれる日付を作成します。 週の終わり(土曜日)を endOfWeek で取得し、 eachDayOfInterval で日曜日から土曜日までの日付を取得します。

  sundays.map(sunday =>
    eachDayOfInterval({start: sunday, end: endOfWeek(sunday)})
  )

以上でカレンダー用の日付の2次元配列を作成することができました。

カレンダーを表示する

カレンダーを表示します。 今回はreactを使用します。 create-react-appで新しいプロジェクトを作成しましょう。

npx create-react-app date-fns-calendar
cd date-fns-calendar
yarn add date-fns
yarn start

getCalendarArrayで得られた配列をテーブル形式で表示します。 各週ごとに日付が格納されているので、2重のmapになっています。

日付は getDate で取得します。コンポーネントのキーは曜日にするので、 getDay で取得します。 getDateとgetDayの名前が紛らわしいので気をつけてください。

また、 format を利用してテーブルの上に年月表示します。

src/App.js

import React from 'react'
import format from 'date-fns/format'
import getDate from 'date-fns/getDate'
import getDay from 'date-fns/getDay'
import eachDayOfInterval from 'date-fns/eachDayOfInterval'
import endOfWeek from 'date-fns/endOfWeek'
import eachWeekOfInterval from 'date-fns/eachWeekOfInterval'
import startOfMonth from 'date-fns/startOfMonth'
import endOfMonth from 'date-fns/endOfMonth'

const getCalendarArray = date => {
  const sundays = eachWeekOfInterval({
    start: startOfMonth(date),
    end: endOfMonth(date)
  })
  return sundays.map(sunday =>
    eachDayOfInterval({start: sunday, end: endOfWeek(sunday)})
  )
}

function App() {
  const targetDate = new Date()
  const calendar = getCalendarArray(targetDate)

  return (
    <div>
      {format(targetDate, 'y年M月')}
      <table>
        <thead>
          <tr>
            <th>日</th><th>月</th><th>火</th><th>水</th><th>木</th><th>金</th><th>土</th>
          </tr>
        </thead>
        <tbody>
          {calendar.map((weekRow, rowNum) => (
            <tr key={rowNum}>
              {weekRow.map(date => (
                <td key={getDay(date)}>{getDate(date)}</td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  )
}

export default App

実行結果です。今月のカレンダーが表示されました。

f:id:yucatio:20191223083802p:plain

前の月と次の月の日付も表示されていますが、今月以外の日付の見た目を変更するのは3つ先の記事で行いますので、今回はこのままです。

ソースコード

ここまでのソースコードです。

GitHub - yucatio/date-fns-calendar at 00c2d86e0307650be1a9ded334797db2ec7b8eb6

環境

  • npm: 6.12.1
  • react: 16.12.0
  • date-fns: 2.8.1

あとがき

以前にrubyで同様のプログラムを作成しました。

yucatio.hatenablog.com

同様のアルゴリズムを使用しようと思いましたが、 eachWeekOfInterval という関数を見つけたことで大幅にコード量を減らせました。

次回の記事

次回は次の月と前の月に移動できるようにします。

yucatio.hatenablog.com

Pythonでひらがなカタカナの国語辞典の見出し語順ソート

前回の記事の続きです。

yucatio.hatenablog.com

今回はひらがなに加えて、カタカナも国語辞典の見出し語順に並べます。

大辞泉の見出しの配列のページから、ひらがなとカタカナに関係ある部分をを引用します。

  1. 見出しは、五十音順に配列した。一字目が同じものは二字目のかなの五十音順とし、以下も同様に扱った。長音符号「ー」は、直前のかなの母音と同じとして扱った。
  2. 同じかなのときは、以下の基準を設けて配列した。
    1. 清音・濁音・半濁音の順。 ハート→ハード→バード→パート
    2. 拗音・促音は、直音の前。 ひょう【表】→ひ‐よう【費用】
    3. 和語・漢語・外来語の順。 めい【×姪】→めい【銘】→メイ【May】

https://dictionary.goo.ne.jp/help/jn/02_03.html

アルゴリズム

ひらがなとカタカナを国語辞典の見出し語順に並び替えるプログラムを作成しましょう。凡例の、

長音符号「ー」は、直前のかなの母音と同じとして扱った。

こちらの実装は、簡単のため、長音符号「ー」の直前の文字はカタカナであることを前提とします。また、「ー」は連続しないことを前提としています。

和語・漢語・外来語の順。

こちらは単純に、それ以前の順位が同じであれば、ひらがな→カタカナの順番で並び替えるという意味とします。 幸い、カタカナはひらがなよりも文字コード上で後なので、ひらがな表記が同じ場合は、元の文字で比較すればよさそうです。

比較の順番は以下のようになります。

  1. カタカナの長音符号「ー」を直前のカタカナの母音に変換し、さらにカタカナをひらがなに変換する。さらに濁音と半濁音を清音に、拗音と促音を直音に変換してソートする
  2. 1の並べ替えで順序が同じ場合は、カタカナの長音符号「ー」を直前のカタカナの母音に変換し、さらにカタカナをひらがなに変換する。さらに拗音と促音を直音に変換してソートする
  3. 2の並び替えでも順序が同じ場合は、カタカナの長音符号「ー」を直前のカタカナの母音に変換し、さらにカタカナをひらがなに変換してソートする
  4. 3の並び替えでも順序が同じ場合は、もとのひらがなとカタカナを使用して並べ替える

なお前回に引き続き、小さい'ゎ' 'ゕ' 'ゖ'、および旧かな使いの'ゐ''ゑ'は今回は考慮の対象外としています。

コード

上記アルゴリズムを実装したPythonのコードです。

import re
import jaconv

DAKU_TO_SEION = {
    'ゔ': 'う',
    'が': 'が', 'ぎ': 'き', 'ぐ': 'く', 'げ': 'け', 'ご': 'こ',
    'ざ': 'さ', 'じ': 'し', 'ず': 'す', 'ぜ': 'せ', 'ぞ': 'そ',
    'だ': 'た', 'ぢ': 'ち', 'づ': 'つ', 'で': 'て', 'ど': 'と',
    'ば': 'は', 'び': 'ひ', 'ぶ': 'ふ', 'べ': 'へ', 'ぼ': 'ほ',
    'ぱ': 'は', 'ぴ': 'ひ', 'ぷ': 'ふ', 'ぺ': 'へ', 'ぽ': 'ほ',
}

SMALL_TO_NORMAL = {
    'ぁ': 'あ', 'ぃ': 'い', 'ぅ': 'う', 'ぇ': 'え', 'ぉ': 'お',
    'っ': 'つ',
    'ゃ': 'や', 'ゅ': 'ゆ', 'ょ': 'よ',
}

KATAKANA_VOWEL = {
    'ァ': 'ア', 'ィ': 'イ', 'ゥ': 'ウ', 'ェ': 'エ', 'ォ': 'オ',
    'ア': 'ア', 'イ': 'イ', 'ウ': 'ウ', 'エ': 'エ', 'オ': 'オ',
    'ヴ': 'ウ',
    'カ': 'ア', 'キ': 'イ', 'ク': 'ウ', 'ケ': 'エ', 'コ': 'オ',
    'ガ': 'ア', 'ギ': 'イ', 'グ': 'ウ', 'ゲ': 'エ', 'ゴ': 'オ',
    'サ': 'ア', 'シ': 'イ', 'ス': 'ウ', 'セ': 'エ', 'ソ': 'オ',
    'ザ': 'ア', 'ジ': 'イ', 'ズ': 'ウ', 'ゼ': 'エ', 'ゾ': 'オ',
    'タ': 'ア', 'チ': 'イ', 'ツ': 'ウ', 'テ': 'エ', 'ト': 'オ',
    'ダ': 'ア', 'ヂ': 'イ', 'ヅ': 'ウ', 'デ': 'エ', 'ド': 'オ',
    'ナ': 'ア', 'ニ': 'イ', 'ヌ': 'ウ', 'ネ': 'エ', 'ノ': 'オ',
    'ハ': 'ア', 'ヒ': 'イ', 'フ': 'ウ', 'ヘ': 'エ', 'ホ': 'オ',
    'バ': 'ア', 'ビ': 'イ', 'ブ': 'ウ', 'ベ': 'エ', 'ボ': 'オ',
    'パ': 'ア', 'ピ': 'イ', 'プ': 'ウ', 'ペ': 'エ', 'ポ': 'オ',
    'マ': 'ア', 'ミ': 'イ', 'ム': 'ウ', 'メ': 'エ', 'モ': 'オ',
    'ャ': 'ア', 'ュ': 'ウ', 'ョ': 'オ',
    'ヤ': 'ア', 'ユ': 'ウ', 'ヨ': 'オ',
    'ラ': 'ア', 'リ': 'イ', 'ル': 'ウ', 'レ': 'エ', 'ロ': 'オ',
    'ワ': 'ア', 'ヲ': 'オ',
}

TO_NORMAL_SEION_TABLE = str.maketrans({**DAKU_TO_SEION, **SMALL_TO_NORMAL})

TO_NORMAL_TABLE = str.maketrans(SMALL_TO_NORMAL)


def dash_to_kana(matchobj):
    """Converts "(A katakana) + 'ー'" to "(A katakana) + (katakana vowel)"
    ex) 'ター' -> 'タア', 'ヒー' -> 'ヒイ'
    """
    # second letter must be dash
    first_letter, dash = matchobj.group(0)
    if first_letter in KATAKANA_VOWEL:
        return first_letter + KATAKANA_VOWEL[first_letter]
    else:
        return first_letter + first_letter


def hiragana(kana):
    dash_translated = re.sub('.ー', dash_to_kana, kana)
    return jaconv.kata2hira(dash_translated)


def sort_as_ja_dict(kana_list):
    return sorted(kana_list,
                  key=lambda kana: (hiragana(kana).translate(TO_NORMAL_SEION_TABLE),
                                    hiragana(kana).translate(TO_NORMAL_TABLE), hiragana(kana), kana))


# 使い方
kana_item_2 = [
    'こおどり',
    'メイ',
    'コード',
    'コーヒー',
    'コート',
    'こおり',
    'めい',
    'かれき',
    'カレー',
    'かれえ',
    'かり',
    'カラー',
    'からい',
]

sort_as_ja_dict(kana_item_2)
# => ['カラー', 'からい', 'かり', 'かれえ', 'カレー', 'かれき', 'コート', 'コード', 'こおどり', 'コーヒー', 'こおり', 'めい', 'メイ']

コードの解説

カタカナの長音符号「ー」を直前のカタカナに変換するのは以下の部分です。

KATAKANA_VOWEL = {
    'ァ': 'ア', 'ィ': 'イ', 'ゥ': 'ウ', 'ェ': 'エ', 'ォ': 'オ',
    'ア': 'ア', 'イ': 'イ', 'ウ': 'ウ', 'エ': 'エ', 'オ': 'オ',
    'ヴ': 'ウ',
    'カ': 'ア', 'キ': 'イ', 'ク': 'ウ', 'ケ': 'エ', 'コ': 'オ',
    'ガ': 'ア', 'ギ': 'イ', 'グ': 'ウ', 'ゲ': 'エ', 'ゴ': 'オ',
    'サ': 'ア', 'シ': 'イ', 'ス': 'ウ', 'セ': 'エ', 'ソ': 'オ',
    'ザ': 'ア', 'ジ': 'イ', 'ズ': 'ウ', 'ゼ': 'エ', 'ゾ': 'オ',
    'タ': 'ア', 'チ': 'イ', 'ツ': 'ウ', 'テ': 'エ', 'ト': 'オ',
    'ダ': 'ア', 'ヂ': 'イ', 'ヅ': 'ウ', 'デ': 'エ', 'ド': 'オ',
    'ナ': 'ア', 'ニ': 'イ', 'ヌ': 'ウ', 'ネ': 'エ', 'ノ': 'オ',
    'ハ': 'ア', 'ヒ': 'イ', 'フ': 'ウ', 'ヘ': 'エ', 'ホ': 'オ',
    'バ': 'ア', 'ビ': 'イ', 'ブ': 'ウ', 'ベ': 'エ', 'ボ': 'オ',
    'パ': 'ア', 'ピ': 'イ', 'プ': 'ウ', 'ペ': 'エ', 'ポ': 'オ',
    'マ': 'ア', 'ミ': 'イ', 'ム': 'ウ', 'メ': 'エ', 'モ': 'オ',
    'ャ': 'ア', 'ュ': 'ウ', 'ョ': 'オ',
    'ヤ': 'ア', 'ユ': 'ウ', 'ヨ': 'オ',
    'ラ': 'ア', 'リ': 'イ', 'ル': 'ウ', 'レ': 'エ', 'ロ': 'オ',
    'ワ': 'ア', 'ヲ': 'オ',
}

def dash_to_kana(matchobj):
    """Converts "(A katakana) + 'ー'" to "(A katakana) + (katakana vowel)"
    ex) 'ター' -> 'タア', 'ヒー' -> 'ヒイ'
    """
    # second letter must be dash
    first_letter, dash = matchobj.group(0)
    if first_letter in KATAKANA_VOWEL:
        return first_letter + KATAKANA_VOWEL[first_letter]
    else:
        return first_letter + first_letter


def hiragana(kana):
    dash_translated = re.sub('.ー', dash_to_kana, kana)
    return jaconv.kata2hira(dash_translated)

長音記号とその直前の文字を取得するのに re.sub(pattern, repl, string, count=0, flags=0)( re --- 正規表現操作 — Python 3.7.6 ドキュメント )を利用します。

pattern'.ー'を指定し、長音記号と直前の文字を取得します。 replに関数(dash_to_kana)を指定し、patternにマッチした文字列を処理します。

dash_to_kanaで、matchobj.group(0)を使用してpatternにマッチした文字列全体を取り出します。

マッチした最初の文字を母音を、KATAKANA_VOWELにそって置換します。

最後に、最初の文字とその母音をつなげて返します。

カタカナからひらがなへの変換はjaconvを利用しました。

def hiragana(kana):
    dash_translated = re.sub('.ー', dash_to_kana, kana)
    return jaconv.kata2hira(dash_translated)

ひらがなとカタカナが入った並び替えでは、途中まではカタカナをひらがなに変換して比較します。 ひらがなかカタカナで順番が決まるのは最後です。

ひらがなのみのときのsort_as_ja_dictは以下でした。

# ひらがなのみ比較バージョン
def sort_as_ja_dict(kana_list):
    return sorted(kana_list,
                  key=lambda kana: (kana.translate(TO_NORMAL_SEION_TABLE),
                                    kana.translate(TO_NORMAL_TABLE), kana))

比較する値のkanaの部分をhiragana(kana)に変換し、最後にkanaによる比較を追加します。

def sort_as_ja_dict(kana_list):
    return sorted(kana_list,
                  key=lambda kana: (hiragana(kana).translate(TO_NORMAL_SEION_TABLE),
                                    hiragana(kana).translate(TO_NORMAL_TABLE), hiragana(kana), kana))

動作を確認してみましょう。 最後にひらがなの後にカタカナが並べられることがわかります。

kana_item_2 = [
    'こおどり',
    'メイ',
    'コード',
    'コーヒー',
    'コート',
    'こおり',
    'めい',
    'かれき',
    'カレー',
    'かれえ',
    'かり',
    'カラー',
    'からい',
]

sorted(kana_item_2)
# => ['からい', 'かり', 'かれえ', 'かれき', 'こおどり', 'こおり', 'めい', 'カラー', 'カレー', 'コート', 'コード', 'コーヒー', 'メイ']

sorted(kana_item_2,
       key=lambda kana: hiragana(kana).translate(TO_NORMAL_SEION_TABLE))
# => ['カラー', 'からい', 'かり', 'カレー', 'かれえ', 'かれき', 'コート', 'コード', 'こおどり', 'コーヒー', 'こおり', 'メイ', 'めい']

sorted(kana_item_2,
       key=lambda kana: (hiragana(kana).translate(TO_NORMAL_SEION_TABLE),
                         hiragana(kana).translate(TO_NORMAL_TABLE)))
# => ['カラー', 'からい', 'かり', 'カレー', 'かれえ', 'かれき', 'コート', 'コード', 'こおどり', 'コーヒー', 'こおり', 'メイ', 'めい']

sorted(kana_item_2,
       key=lambda kana: (hiragana(kana).translate(TO_NORMAL_SEION_TABLE),
                         hiragana(kana).translate(TO_NORMAL_TABLE), hiragana(kana)))
# => ['カラー', 'からい', 'かり', 'カレー', 'かれえ', 'かれき', 'コート', 'コード', 'こおどり', 'コーヒー', 'こおり', 'メイ', 'めい']

sorted(kana_item_2,
       key=lambda kana: (hiragana(kana).translate(TO_NORMAL_SEION_TABLE),
                         hiragana(kana).translate(TO_NORMAL_TABLE), hiragana(kana), kana))
# => ['カラー', 'からい', 'かり', 'かれえ', 'カレー', 'かれき', 'コート', 'コード', 'こおどり', 'コーヒー', 'こおり', 'めい', 'メイ']

以上でひらがなとカタカナを国語辞典の見出し語順に並び替えることができました。

環境