用 Ramda 做简易日历

效果

仓库地址 github.com/ShiJinlong1...

用法

js 复制代码
import * as BaseCalendar from './base-calendar/index.js';

function main() {
  BaseCalendar.StyleSheet().then(
    sheet => document.adoptedStyleSheets.push(sheet)
  );
  document.body.append(
    BaseCalendar.Component({ date: new Date() }).identity,
  );
}

main();

规格

七列。

七行,一行表头 一二三四五六日;六行日期。

组成

显示上个月、本月和下个月的天数。

需要的日数据

  1. 上个月天数最后的几天
  2. 这个月的所有天数。
  3. 下个月的所有天数。

到底补上个月最后几天,取决于本月 1 日的 周几 - 1

比如 2023 年 12 月,1 日在周五,所以补充上个月的最后四天。

最后拼接好三段数组,截取出前 42 个。

概览

  • BaseCalendar.Component()

    组件函数,会返回必要的元素节点和函数。

  • identity: DocumentFragment

    本体。

  • state

    必要的状态和 HTML 元素。

js 复制代码
// base-calendar/index.js

import { ElemsHTML, Frag } from '@/util/dom.js';

const tableSize = { row: 6, col: 7 };

export function StyleSheet() { ... }

export function Component(props) {
  function render() { ... }
  function init() { ... }
  
  const identity = Frag(
    `<table class="base-calendar">
      <thead>
        <tr>
        ${ ElemsHTML(R.map(s => `<th>${ s }</th>`), ['一', '二', '三', '四', '五', '六', '日']) }
        </tr>
      </thead>
      <tbody><!-- --></tbody>
    </table>`
  )
  .firstElementChild;

  const state = {
    date: new Date(),
    get tds() {
      return identity.querySelectorAll('tbody td');
    },
  };
  
  init();
  render();
  
  return {
    identity,
    render,
  }
}

样式

js 复制代码
// base-calendar/index.js

export function StyleSheet() {
  const sheet = new CSSStyleSheet();
  return sheet.replace(`
    .base-calendar {
      text-align: center;
      border-collapse: collapse;

      &, th, td { border: 1px solid currentColor; }

      td {
        width: 2em;
        height: 2em;
      }
    }
  `)
}

初始化

props 初始化 state,给 <tbody> 里添加空的 <td>

js 复制代码
// base-calendar/index.js

import { ElemsHTML, Frag } from '@/util/dom.js';

export function Component(props) {
  function render() { ... }
  
  function init() {
    props.date && (state.date = props.date);
    
    identity.querySelector('tbody').innerHTML = ElemsHTML(
      R.map(() => (
        `<tr>${
          ElemsHTML(R.map(() => `<td></td>`), R.range(0, tableSize.col))
        }</tr>`)
      ),
      R.range(0, tableSize.row)
    );
  }
  
  const identity = Frag(
    /* ... */
  )
  .firstElementChild;
  
  const state = {
    /* ... */
  };
  
  init();
  render();
  
  return {
    identity,
    render,
  };
}

初步成果

渲染

用日期数字填充所有 <td>

本月1日在第 1 行 周x 列。

本月日期之前和之后,分别填上个月和下个月的。

  • DateInfoDic()

    上本下三个月的相关信息。

  • DatesDic

    上本下三个月的日期数组。

js 复制代码
import { DateInfoDic, DatesDic } from './use-date-info.js';

export function Component(props) {
  function render() {
    const dateInfoDic = DateInfoDic(state.date);
    const datesDic = DatesDic(dateInfoDic);

    R.compose(
      R.addIndex(R.forEach)((n, i) => (state.tds[i].innerText = n)),
      R.take(state.tds.length),
      R.unnest
    )([
      /* 上个月天数往回推 本月1日的周x 个,再 +1 */
      R.slice(
        datesDic.prevMonth.length - dateInfoDic.currMonth.dayOfWeek + 1,
        Number.POSITIVE_INFINITY,
        datesDic.prevMonth
      ),
      datesDic.currMonth,
      datesDic.nextMonth
    ]))
  }
  
  function init() {
    /* ... */
  }
  
  const identity = Frag(
    /* ... */
  )
  .firstElementChild;
  
  const state = {
    /* ... */
  };
  
  init();
  render();
  
  return {
    identity,
    render,
  };
}

日期信息

js 复制代码
import { DayNumOfMonth } from '@/util/date.js';

/**
 * 年、月下标、日期、周日期
 * @param {Date} date 
 * @returns {import('./type').DateInfo}
 */
function DateInfo(date) {
  return {
    year: date.getFullYear(),
    monthIndex: date.getMonth(),
    dayOfMonth: date.getDate(),
    /** getDay() 其他都是,1-6,周日得把 0 变 7`, */
    dayOfWeek: date.getDay() || 7,
  };
}

/** x年x月1日 `Date` */
const FstDateOfMonth = ({ year, monthIndex }) => new Date(year, monthIndex);

/** `DateInfo` 转成相邻月份1日 */
const toAdjacentMonth = (monthIndexCond, TF, RF) => (
  R.ifElse( R.compose( monthIndexCond, R.prop('monthIndex') ), TF, RF )
);

/**
 * x年x月1日 `DateInfo`
 * @param {Function} f 
 * @param {Date} date 
 * @returns {import('./type').DateInfo}
 */
const FstDateInfoOfMonth = (f, date) => R.compose( DateInfo, FstDateOfMonth, f, DateInfo )(date);

/**
 * 本月和相邻月份1日 `Date`
 * @param {Date} date
 * @returns {import('./type').DateInfoDic}
 */
export const DateInfoDic = date => ({
  get prevMonth() {
    return FstDateInfoOfMonth(
      toAdjacentMonth(
        R.gt(R.__, 0),
        R.evolve({ monthIndex: R.dec }),
        R.evolve({ year: R.dec, monthIndex: R.always(11) })
      ),
      date
    )
  },

  get currMonth() { return FstDateInfoOfMonth(R.identity, date) },

  get nextMonth() {
    return FstDateInfoOfMonth(
      toAdjacentMonth(
        R.lt(R.__, 11),
        R.evolve({ monthIndex: R.inc }),
        R.evolve({ year: R.inc, monthIndex: R.always(0) })
      ),
      date
    )
  },
});

/**
 * @type {function(import('./type').DateInfoDic): Record<import('./type').NeedMonth, number[]>}
 */
export const DatesDic = R.map(R.compose( R.range(1), R.inc, DayNumOfMonth ));

工具函数

util/dom

  • ElemsHTML()

    string[] -> string

  • Frag()

    创建 <template>,赋值 innerHTML,返回 content

js 复制代码
// util/dom.js

/** @type {function( transducer: Function, any[]): string} */
export const ElemsHTML = R.transduce(R.__, R.concat, '', R.__);

/** @type {(html: any) => DocumentFragment} */
export function Frag(html) {
  const tmplElem = document.createElement('template');
  tmplElem.innerHTML = html;
  return tmplElem.content;
};

util/date

  • isLeapYear()

    判断年份是否为闰年

  • DayNumOfMonth()

    算出月份的天数

js 复制代码
// util/date.js

/** @type {(year: number) => boolean} */
export const isLeapYear = R.anyPass([
  R.compose( R.equals(0), R.modulo(R.__, 400) ),
  R.allPass([
    R.compose( R.equals(0), R.modulo(R.__, 4) ),
    R.compose( R.not, R.equals(0), R.modulo(R.__, 100) )
  ])
]);

/** @type {(month: number) => number} */
export const DayNumOfMonth = R.cond([
  [R.compose( R.includes(R.__, [1,3,5,7,8,10,12]), R.inc, R.prop('monthIndex') ), R.always(31)],
  [R.compose( R.includes(R.__, [4,6,9,11]),        R.inc, R.prop('monthIndex') ), R.always(30)],
  [R.T, R.ifElse( R.compose( isLeapYear, R.prop('year') ), R.always(29), R.always(28) )]
]);
相关推荐
异常君5 天前
Java 双冒号(::)操作符实战解析与类型推断机制
java·代码规范·函数式编程
安冬的码畜日常1 个月前
【玩转 JS 函数式编程_016】DIY 实战:巧用延续传递风格(CPS)重构倒计时特效逻辑
开发语言·前端·javascript·重构·函数式编程·cps风格·延续传递风格
zidea2 个月前
Rust 闭包:捕获环境的魔法函数
rust·ai编程·函数式编程
独泪了无痕3 个月前
Optional 使用指南:彻底告别 NPE
后端·函数式编程
doodlewind3 个月前
通过 TypeScript 类型体操学习日语语法
typescript·编程语言·函数式编程
谦谦橘子3 个月前
rxjs原理解析
前端·javascript·函数式编程
桦说编程4 个月前
【硬核总结】如何轻松实现只计算一次、惰性求值?良性竞争条件的广泛使用可能超过你的想象!String实际上是可变的?
后端·函数式编程
Oberon4 个月前
从零开始的函数式编程(2) —— Church Boolean 编码
数学·函数式编程·λ演算
桦说编程4 个月前
CompletableFuture 超时功能有大坑!使用不当直接生产事故!
java·性能优化·函数式编程·并发编程