Last active
May 27, 2022 07:32
-
-
Save Jessidhia/dc117754ec668421eadb60532646a0a7 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// @ts-check | |
// ==UserScript== | |
// @name Export Suica transactions to CSV | |
// @namespace http://tampermonkey.net/ | |
// @version 0.2 | |
// @icon https://www.jreast.co.jp/favicon.ico | |
// @description Export Suica transactions to CSV | |
// @updateURL https://gist.github.com/Jessidhia/dc117754ec668421eadb60532646a0a7/raw/suica-script.user.js | |
// @author Jessidhia | |
// @include https://www.mobilesuica.com/iq/ir/SuicaDisp.aspx* | |
// @grant none | |
// ==/UserScript== | |
;(function () { | |
'use strict' | |
// Could be "JR East", but the card can be used anywhere so use a generic name. | |
// It's in a const here so it can be changed. | |
const TransportationPayee = 'Suica Transport' | |
const buttonRow = document.querySelector( | |
'.historyBox .grybg01 tr .rightElm' | |
) | |
if (!buttonRow || !buttonRow.children[0]) { | |
return | |
} | |
const button = document.createElement('button') | |
button.type = 'button' | |
button.className = 'list_title' | |
button.style.marginRight = '0.5em' | |
button.textContent = 'Export to CSV' | |
const titleRow = document.querySelector('.historyTable tr.NoLine') | |
if (!titleRow) { | |
return | |
} | |
const titleData = Array.from(titleRow.children, td => td.textContent) | |
// ["月/日","種別","利用場所","種別","利用場所","残額","差額"] | |
if ( | |
JSON.stringify(titleData) !== | |
`["","月日","種別","利用場所","種別","利用場所","残高","入金・利用額"]` | |
) { | |
alert('Suica table format changed; aborting') | |
return | |
} | |
buttonRow.insertBefore(button, buttonRow.children[0]) | |
button.addEventListener('click', () => { | |
const rawDataRows = Array.from( | |
// .NoLine is the titleRow we just validated before starting | |
// :last-child is just an initial balance value and not useful to us | |
document.querySelectorAll( | |
'.historyTable tr:not(.NoLine):not(:last-child)' | |
), | |
tr => | |
Array.from(tr.children, td => | |
// Replace full-width spaces with half-width, trim the excess | |
// prettier-ignore | |
/** @type {string} */ (td.textContent).replace(/ /g, ' ').trim() | |
) | |
) | |
const csvTitleRow = ['Date', 'Payee', 'Memo', 'Amount'] | |
const dataRows = rawDataRows.map( | |
([, date, type, location, kind, exitLocation, , amountStr]) => { | |
const amount = amountStr.replace(/,/g, '') | |
if (!location) { | |
// We just know it was a transation but no idea from where or what | |
return [date, '', '', amount] | |
} | |
if (!kind) { | |
// Probably a charge from credit card | |
if (amount.startsWith('+')) { | |
// keep type + location in the same field for use with payee renaming feature | |
return [date, `Charge ${type} ${location}`, '', amount] | |
} else { | |
return [date, `${type} ${location}`, '', amount] | |
} | |
} | |
if (kind) { | |
// A transportation fare | |
return [ | |
date, | |
TransportationPayee, | |
`${type} ${location} ${kind} ${exitLocation}`, | |
amount, | |
] | |
} | |
return [date, 'Format Error', 'Unknown', amount] | |
} | |
) | |
exportToCsv('suica.csv', [csvTitleRow, ...dataRows]) | |
}) | |
// refactored from https://stackoverflow.com/a/24922761 | |
/** | |
* @typedef {null|string|number|Date} CSVData | |
* @param {string} filename | |
* @param {ReadonlyArray<ReadonlyArray<CSVData>>} rows | |
*/ | |
function exportToCsv(filename, rows) { | |
const csvFile = rows | |
.map(row => | |
row | |
.map(item => { | |
const itemString = | |
item === null | |
? '' | |
: item instanceof Date | |
? item.toLocaleString() | |
: item.toString() | |
const escaped = itemString.replace(/"/g, '""') | |
return /("|,|\n)/.test(escaped) ? `"${escaped}"` : escaped | |
}) | |
.join(',') | |
) | |
.join('\n') | |
const blob = new Blob([csvFile], { type: 'text/csv;charset=utf-8;' }) | |
const link = document.createElement('a') | |
const url = URL.createObjectURL(blob) | |
link.setAttribute('href', url) | |
link.setAttribute('download', filename) | |
link.style.visibility = 'hidden' | |
document.body.appendChild(link) | |
link.click() | |
document.body.removeChild(link) | |
} | |
})() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment