MediaWiki:Gadget-fa-bid-calculator.js: Difference between revisions

Lower-case years/points in the bid format
The league is NBA RealGM
 
(5 intermediate revisions by the same user not shown)
Line 1: Line 1:
/**
/**
  * 1TC RealGM — free agency bid calculator.
  * NBA RealGM — free agency bid calculator.
  * Implements the points system on [[NBA:Free Agency]]. If the two ever disagree,
  * Implements the points system on [[NBA:Free Agency]]. If the two ever disagree,
  * the wiki page is the rule and this script is the bug.
  * the wiki page is the rule and this script is the bug.
Line 11: Line 11:
var MIN_OFFER = 500000,
var MIN_OFFER = 500000,
MIN_RAISE = 200000,
MIN_RAISE = 200000,
MIN_RAISE_POINTS = 0.2,
MAX_YEARS = 4,
MAX_YEARS = 4,
MAX_YEARS_35 = 2,
MAX_YEARS_35 = 2,
YEAR_POINTS = 1.5;
YEAR_POINTS = 1.5;


function money( n ) {
function money( n ) {
return '$' + n.toLocaleString( 'en-US' );
return '$' + n.toLocaleString( 'en-US' );
}
/** Digits only, so "4,000,000" and "4000000" both read as 4000000. */
function parseMoney( v ) {
var digits = String( v ).replace( /[^0-9]/g, '' );
return digits === '' ? NaN : Number( digits );
}
/**
* Reformat a text input with thousands separators, keeping the caret where
* the typist left it. A plain reformat would send it to the end, which makes
* editing the middle of a figure unusable.
*/
function formatWithCaret( el ) {
var before = el.value.slice( 0, el.selectionStart ),
digitsBefore = before.replace( /[^0-9]/g, '' ).length,
n = parseMoney( el.value );
el.value = isNaN( n ) ? '' : n.toLocaleString( 'en-US' );
// Walk forward past the same number of digits to place the caret.
var pos = 0, seen = 0;
while ( pos < el.value.length && seen < digitsBefore ) {
if ( /[0-9]/.test( el.value[ pos ] ) ) {
seen++;
}
pos++;
}
el.setSelectionRange( pos, pos );
}
}


Line 97: Line 128:
form.className = 'tc-calc';
form.className = 'tc-calc';


/**
* One labelled row. Controls live in their own flex wrapper so that on a
* narrow screen the label can stack above them while they stay in a row.
* Returns the wrapper, so callers can append a readout beside the control.
*/
function row( labelText, el ) {
function row( labelText, el ) {
var r = document.createElement( 'div' );
var r = document.createElement( 'div' );
Line 102: Line 138:
var l = document.createElement( 'label' );
var l = document.createElement( 'label' );
l.textContent = labelText;
l.textContent = labelText;
var c = document.createElement( 'div' );
c.className = 'tc-calc-controls';
c.appendChild( el );
r.appendChild( l );
r.appendChild( l );
r.appendChild( el );
r.appendChild( c );
form.appendChild( r );
form.appendChild( r );
return r;
return c;
}
}


var years = document.createElement( 'select' );
var years = document.createElement( 'select' );
// A number input cannot display separators — browsers reject a comma as
// invalid number syntax — so this is a text input formatted by hand.
var salary = document.createElement( 'input' );
var salary = document.createElement( 'input' );
salary.type = 'number';
salary.type = 'text';
salary.step = String( MIN_RAISE );
salary.inputMode = 'numeric';
salary.min = String( MIN_OFFER );
salary.autocomplete = 'off';
salary.value = String( MIN_OFFER );
salary.className = 'tc-calc-money';
salary.value = MIN_OFFER.toLocaleString( 'en-US' );
salary.addEventListener( 'input', function () {
formatWithCaret( salary );
} );


var age35 = document.createElement( 'input' );
var age35 = document.createElement( 'input' );
Line 126: Line 171:
name.type = 'text';
name.type = 'text';
name.placeholder = 'Caldwell Jones';
name.placeholder = 'Caldwell Jones';
name.style.width = '14em';
name.className = 'tc-calc-wide';


var team = document.createElement( 'input' );
var team = document.createElement( 'input' );
team.type = 'text';
team.type = 'text';
team.placeholder = 'Suns';
team.placeholder = 'Sonics';
team.style.width = '14em';
team.className = 'tc-calc-wide';


function fillYears() {
function fillYears() {
Line 149: Line 194:
var yearPts = document.createElement( 'span' );
var yearPts = document.createElement( 'span' );
yearPts.className = 'tc-calc-inline';
yearPts.className = 'tc-calc-inline';
row( 'Years', years ).appendChild( yearPts );
row( 'Contract length', years ).appendChild( yearPts );


var salaryPts = document.createElement( 'span' );
var salaryPts = document.createElement( 'span' );
salaryPts.className = 'tc-calc-inline';
salaryPts.className = 'tc-calc-inline';
row( 'Salary', salary ).appendChild( salaryPts );
row( 'Salary', salary ).appendChild( salaryPts );
var ageRow = row( 'Player is 35+', age35 );
row( 'Player is 35+', age35 ).parentNode
ageRow.querySelector( 'label' ).style.fontWeight = 'normal';
.querySelector( 'label' ).style.fontWeight = 'normal';
row( 'Bid to beat (optional)', beat );
row( 'Bid to beat (optional)', beat );
row( 'Your team (optional)', team );
row( 'Your team (optional)', team );
Line 166: Line 211:
function render() {
function render() {
var y = Number( years.value ),
var y = Number( years.value ),
s = Number( salary.value ),
s = parseMoney( salary.value ),
max = age35.checked ? MAX_YEARS_35 : MAX_YEARS;
max = age35.checked ? MAX_YEARS_35 : MAX_YEARS;


Line 213: Line 258:
box.className = 'tc-calc-note';
box.className = 'tc-calc-note';


if ( total > target ) {
// A bid must clear the standing one by at least MIN_RAISE_POINTS, so
// the bar is target + 0.2, not merely more than target. Everything
// here is counted in tenths of a point: 0.1 points is exactly
// $100,000 and a year is exactly 15 tenths, so integer arithmetic
// gets the answer that 0.2 + 2.0 in binary floating point does not.
var requiredTenths = Math.round( target * 10 ) + ( MIN_RAISE_POINTS * 10 );
 
if ( Math.round( total * 10 ) >= requiredTenths ) {
box.textContent = 'This already beats ' + pts( target ) + ' points.';
box.textContent = 'This already beats ' + pts( target ) + ' points.';
} else {
} else {
// cheapest money-only raise, in $200,000 steps
var salaryTenths = requiredTenths - y * ( YEAR_POINTS * 10 ),
var needed = ( target - total ) * 1000000,
newSalary = Math.max( salaryTenths * 100000, MIN_OFFER );
raises = Math.max( 1, Math.ceil( needed / MIN_RAISE ) ),
newSalary = s + ( raises * MIN_RAISE );
while ( points( y, newSalary ) <= target ) {
newSalary += MIN_RAISE;
}
box.textContent = 'To beat ' + pts( target ) + ' points at ' + y +
box.textContent = 'To beat ' + pts( target ) + ' points at ' + y +
( y === 1 ? ' year' : ' years' ) + ': ' + money( newSalary ) +
( y === 1 ? ' year' : ' years' ) + ': ' + money( newSalary ) +
Line 232: Line 279:
use.textContent = 'Use ' + money( newSalary );
use.textContent = 'Use ' + money( newSalary );
use.addEventListener( 'click', function () {
use.addEventListener( 'click', function () {
salary.value = String( newSalary );
salary.value = newSalary.toLocaleString( 'en-US' );
render();
render();
} );
} );
Line 241: Line 288:


// --- ready-to-paste post ---
// --- ready-to-paste post ---
// "Suns offer Caldwell Jones 2 years @ $500,000 = 3.5 points".
// "Sonics offer Caldwell Jones 2 years @ $500,000 = 3.5 points".
// Whatever of team/player is missing simply drops out.
// Whatever of team/player is missing simply drops out.
var tm = team.value.trim(),
var tm = team.value.trim(),