您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds virtual subaccounts to Yodlee Moneycenter
// ==UserScript== // @name Yodlee Virtual Subaccounts // @namespace http://www.arthaey.com // @description Adds virtual subaccounts to Yodlee Moneycenter // @include https://moneycenter.yodlee.com/moneycenter/accountSummary.moneycenter.do* // @include https://moneycenter.yodlee.com/moneycenter/networth.moneycenter.do* // @include https://moneycenter.yodlee.com/moneycenter/dashboard.moneycenter.do* // @version 1.3 // // Backed up from http://userscripts.org/scripts/review/11674 // Last updated on 2007-09-19 // ==/UserScript== /* HOW TO USE: * * By setting an account's caption/description with a specially formatted string, * you can have virtual subaccounts. The string format is: * * My First Subaccount XX% $X,XXX.XX max; * | | | | * '-> name | | `-> optional (see below) * | | * | `-> dollar goal * | * `-> percentage * * You can have have multiple subaccounts. You need to have either a percentage * or a specific dollar goal; you may have both, but that's optional. The * percentage limits the value of the subaccount to a fraction of the real * account's value. The dollar goal limits the value of the subaccount to a * set dollar amount. * * The "max" is optional. Without it, the subaccount's value will be the * minimum of its percentage and goal. With it, the value will be the maximum. * * The value of the real account is distributed among the virtual subaccounts * in order, trying to completely satisfy the first subaccount before * distributing any funds to the second subaccount, and so on. Keep this is * mind when you define the order of your subaccounts, especially if you use * the "max" setting. * * EXAMPLE: * * Emergency Fund 50% $12,000; Laptop 25% $2000 max; Travel 20%; Other $100; * * CHANGELOG: * v1.3 - added subaccounts to the Dashboard page's Net Worth module * v1.2 - added subaccounts to the Net Worth Statement page * v1.1 - updated to work with Yodlee 8.0 * v1.0 - initial release (subaccounts only on the Accounts Summary page) * */ window.addEventListener("load", function(){ var DEBUG = false; /* UTILITY FUNCTIONS *****************************************************/ function debug(msg) { if (DEBUG) console.log("DEBUG: " + msg); } /* Finds elements whose id matches the given regexp. */ function getElementsByIdRegExp(regex, restrict) { var matchingElements = []; if (!regex) return matchingElements; //if (restrict != "id" && restrict != "class") restrict = null; var elements = document.getElementsByTagName("*"); var element; for (var i = 0; i < elements.length; i++) { element = elements[i]; if (element.id.match(regex)) { matchingElements.push(element); } } return matchingElements; } /* * Written by Jonathan Snook, http://www.snook.ca/jonathan * Add-ons by Robert Nyman, http://www.robertnyman.com */ function getElementsByClassName(className, tag, elm){ var testClass = new RegExp("(^|\\s)" + className + "(\\s|$)"); var tag = tag || "*"; var elm = elm || document; var elements = (tag == "*" && elm.all)? elm.all : elm.getElementsByTagName(tag); var returnElements = []; var current; var length = elements.length; for(var i=0; i<length; i++){ current = elements[i]; if(testClass.test(current.className)){ returnElements.push(current); } } return returnElements; } // returns cents function stringToMoney(moneyStr) { if (!moneyStr) return null; // convert to string, if necessary if (!moneyStr.replace) { moneyStr = moneyStr.toString(); } // remove any non-digit characters, excepting "." moneyStr = moneyStr.replace(/[^0-9.]/g, ''); // add cents to even dollar amounts if (!moneyStr.match(/[.]/)) { moneyStr += ".00"; } // convert to an integer amount of cents return Math.round(parseFloat(moneyStr) * 100); } function moneyToString(money) { var cents = Math.round(money); var even = (cents % 100 == 0); var moneyStr = new Number(Math.round(cents) / 100).toLocaleString(); return "$" + moneyStr + (even ? ".00" : ""); } String.prototype.trim = function() { return this.replace(/^\s+|\s+$/, ''); }; /* VIRTUAL SUBACCOUNTS OBJECT ********************************************/ const Site = new Object(); Site.BASE_URL = "https://moneycenter.yodlee.com/moneycenter/"; Site.ACCOUNT_SUMMARY = Site.BASE_URL + "accountSummary.moneycenter.do"; Site.NET_WORTH = Site.BASE_URL + "networth.moneycenter.do"; Site.DASHBOARD = Site.BASE_URL + "dashboard.moneycenter.do"; Site.page = null; Site.determinePage = function() { var url = window.location.href; var pages = [Site.ACCOUNT_SUMMARY, Site.NET_WORTH, Site.DASHBOARD]; for (var i in pages) { var page = pages[i]; if (url.match('^' + page)) { Site.page = page; break; } } debug("Site.page == " + Site.page); } const Subaccounts = new Object(); Subaccounts.all = []; Subaccounts.parseCaption = function(fullCaption, parentAccount) { var name, percent, goal; name = percent = goal = null; fullCaption = fullCaption.trim(); var captions = fullCaption.split(";"); var matches, subaccount; var thisParse = []; for (var i = 0; i < captions.length; i++) { matches = captions[i].match(SUBACCOUNTS); if (matches) { name = matches[NAME_NDX]; goal = matches[GOAL_NDX] || matches[GOAL_ONLY_NDX]; if (goal) { goal = stringToMoney(goal); } percent = matches[PERCENT_NDX] || matches[PERCENT_ONLY_NDX]; if (percent) { percent /= 100; } subaccount = new Subaccount(name, percent, goal, parentAccount); modifiers = matches[MODIFIERS_NDX]; if (modifiers == "max") { subaccount.max = true }; thisParse.push(subaccount); this.all.push(subaccount); } } return thisParse; }; function Subaccount(name, percent, goal, parentAccount) { this.name = name; this.percent = percent; this.goal = goal; this.parentAccount = parentAccount; this.amount = null; this.max = false; this.toString = function() { return this.name + " " + moneyToString(this.amount); }; this.settingsHTML = function() { var content; var html = document.createElement("span"); if (!this.percent && !this.goal) return html; if (this.percent) { var percent = (this.percent * 100) + "%"; if (this.amount >= this.percent * this.parentAccount.amount) { content = document.createElement("b"); content.appendChild(document.createTextNode(percent)); } else { content = document.createTextNode(percent); } html.appendChild(content); } if (this.percent && this.goal) { content = (this.max ? " or " : " only "); html.appendChild(document.createTextNode(content)); } if (this.goal) { if (this.amount >= this.goal) { content = document.createElement("b"); content.appendChild(document.createTextNode( moneyToString(this.goal))); html.appendChild(document.createTextNode("up to ")); html.appendChild(content); } else { var goal = "up to " + moneyToString(this.goal); html.appendChild(document.createTextNode(goal)); } } return html; }; } const Accounts = new Object(); Accounts.parseAmount = function(tableRow) { var cellNdx; switch (Site.page) { case Site.ACCOUNT_SUMMARY: cellNdx = 2; break; case Site.NET_WORTH: case Site.DASHBOARD: cellNdx = 1; break; default: return null; } var amountTD = tableRow.getElementsByTagName("td")[cellNdx]; return stringToMoney(amountTD.textContent); }; function Account(name) { this.name = name; this.subaccounts = null; this.captionDiv = null; this.amount = 0; this.amountUnassigned = 0; this.toString = function() { return this.name + " (" + this.subaccounts.length + " subaccounts)"; }; this.addSubaccountRows = function() { if (this.captionDiv == null) return; // create a fake subaccount for all unassigned, "leftover" money this.distributeFunds(); var unassigned = new Subaccount("Unassigned"); unassigned.amount = this.amountUnassigned; var subaccounts = Array.concat(this.subaccounts, [unassigned]); var subaccount, row, nameCell, amountCell, settingsCell; var subaccountTable = document.createElement("table"); // create table headers row = document.createElement("tr"); nameHeader = document.createElement("th"); amountHeader = document.createElement("th"); settingsHeader = document.createElement("th"); nameHeader.appendChild(document.createTextNode("Subaccount")); amountHeader.appendChild(document.createTextNode("Value")); settingsHeader.appendChild(document.createTextNode("Settings")); row.appendChild(nameHeader); row.appendChild(amountHeader); row.appendChild(settingsHeader); subaccountTable.appendChild(row); // create row for each subaccount for (var i = 0; i < subaccounts.length; i++) { subaccount = subaccounts[i]; row = document.createElement("tr"); nameCell = document.createElement("td"); amountCell = document.createElement("td"); settingsCell = document.createElement("td"); nameCell.appendChild(document.createTextNode(subaccount.name)); amountCell.appendChild(document.createTextNode( moneyToString(subaccount.amount))); settingsCell.appendChild(subaccount.settingsHTML()); nameCell.style.width = "100%"; if (subaccount.name == "Unassigned") { nameCell.style.fontStyle = "italic"; } amountCell.style.textAlign = "right"; amountCell.style.whiteSpace = "nowrap"; settingsCell.style.whiteSpace = "nowrap"; row.appendChild(nameCell); row.appendChild(amountCell); row.appendChild(settingsCell); subaccountTable.appendChild(row); } // add new subaccounts table and remove the original caption this.captionDiv.parentNode.insertBefore( subaccountTable, this.captionDiv.nextSibling); this.captionDiv.parentNode.removeChild(this.captionDiv); this.captionDiv = null; }; this.distributeFunds = function() { var amountLeft = this.amount; var subaccount, amount; for (var i = 0; i < this.subaccounts.length; i++) { subaccount = this.subaccounts[i]; amount = null; if (subaccount.max) { var want = Math.max(subaccount.percent * this.amount, subaccount.goal); amount = Math.min(want, amountLeft); } else { if (subaccount.percent) { amount = Math.min(subaccount.percent * this.amount, amountLeft); } if (subaccount.goal) { amount = Math.min(subaccount.goal, (subaccount.percent ? amount : amountLeft)); } } amountLeft -= amount; subaccount.amount = amount; } this.amountUnassigned = amountLeft; }; } /* VIRTUAL SUBACCOUNTS REGULAR EXPRESSIONS *******************************/ // name (maybe multi-word), not followed by '%', followed by whitespace const NAME = "(\\w+(?:\\s+\\w+)*)(?!%)(?=\\s+)"; // numbers, followed by '%' const PERCENT = "(\\d+)(?:%)"; // '$', followed by numbers (maybe comma-separated), maybe with cents const GOAL = "[$]((?:\\d{1,3},?)*\\d{1,3}(?:[.]\\d{2})?)"; // both PERCENT and GOAL, or just one or the other const PERCENT_AND_OR_GOAL = "(?:" + PERCENT + "\\s+" + GOAL + "|" + PERCENT + "|" + GOAL + ")"; const MODIFIERS = "(max)?"; // optional whitespace const WS = "\\s*"; // subaccount is "NAME PERCENT GOAL"; one of PERCENT or GOAL can be optional const SUBACCOUNT = WS + NAME + WS + PERCENT_AND_OR_GOAL + WS + MODIFIERS + WS; // whole string is a series of subaccounts, separated by semicolons or EOL const SUBACCOUNTS = "^(?:" + SUBACCOUNT + "(?:;|$))+"; // indices for array returned by match(SUBACCOUNT) const ENTIRE_MATCH_NDX = 0; const NAME_NDX = 1; const PERCENT_NDX = 2; const GOAL_NDX = 3; const PERCENT_ONLY_NDX = 4; const GOAL_ONLY_NDX = 5; const MODIFIERS_NDX = 6; /* VIRTUAL SUBACCOUNTS FUNCTIONS *****************************************/ var MAX_TRIES = 3; var numTries = 0; function createSubaccounts() { var accountsWithVirtualSubaccounts = []; var accountsTable; switch (Site.page) { case Site.DASHBOARD: var div = document.getElementById("net_worth_module_dynamic"); accountsTable = getElementsByClassName("datatable", "table", div)[0]; // the dynamic Net Worth module on the dashboard can take a // while to load, so we'll try again later. if (!accountsTable && numTries++ < MAX_TRIES) { debug("Expected table not found yet. Will try to create subaccounts " + (MAX_TRIES - numTries) + " more times..."); window.setTimeout(doVirtualSubaccounts, 2000); return; } break; case Site.ACCOUNT_SUMMARY: case Site.DASHBOARD: accountsTable = document.getElementById("accntsummary"); break; default: return; } var accounts = getElementsByClassName("lcell", "td", accountsTable); if (!accounts) return; debug(accounts.length + " accounts found, total"); var accountTD, name, captionDivs, caption, subaccount; var pattern = new RegExp(SUBACCOUNTS); for (var i = 0; i < accounts.length; i++) { accountTD = accounts[i]; name = parseAccountName(accountTD); debug("Account " + i + " name: " + name); captionDivs = getElementsByClassName("caption", "span", accountTD); if (captionDivs.length > 0) { // if the caption is formatted as required, assume it's meant // to be a virtual subaccount used by this Greasemonkey script caption = captionDivs[0].innerHTML.trim(); debug("Account " + i + " caption: " + caption); if (pattern.test(caption)) { account = new Account(name); account.captionDiv = captionDivs[0]; account.amount = Accounts.parseAmount(accountTD.parentNode); // if we couldn't parse the amount, then skip this account, // even though its description matches the correct format if (!account.amount) { debug("Could not create subaccounts for this account."); continue; } account.subaccounts = Subaccounts.parseCaption(caption, account); accountsWithVirtualSubaccounts.push(account); } } } return accountsWithVirtualSubaccounts; } function parseAccountName(accountTD) { var links = accountTD.getElementsByTagName("a"); if (!links) return null; var nameLink = links[0]; if (!nameLink) return null; return nameLink.textContent.trim().replace(/(\n|\r)+/g, ''); } function prettifyCaptions(captionDiv) { captionDiv.innerHTML = null; } function doVirtualSubaccounts() { var accounts = createSubaccounts(); if (!accounts) return; for (var i = 0; i < accounts.length; i++ ) { accounts[i].addSubaccountRows(); } } Site.determinePage(); doVirtualSubaccounts(); }, true);