# Mechanical Coding

The goal is to calculate the output speed of a series of cogs. The winner will be chosen by popular vote.

The arrangement of cogs is defined in an ASCII-art string:

O large cog
o medium cog
* small cog
- axle (one or more in a row)
= chain (one or more in a row)

An example input is:

o*-O*==oO


o medium cog, meshed with * small cog, attached by - axle to O large cog, meshed with * small cog, connected by == chain to medium cog, meshed with O large cog.

The code takes five inputs by command line argument, standard in, or function parameter:

Arrangement
Number of teeth on large cogs
Number of teeth on medium cogs
Number of teeth on small cogs
Left-side speed in teeth per second (+ve is clockwise, -ve is anti-clockwise)

And outputs:

Right-side speed in teeth per second

Interactions between cogs and other components occur as follows:

SL = Speed of left cog
SR = Speed of right cog
TL = Teeth of left cog
TR = Teeth of right cog

### Two meshed cogs

O*
SR = -SL

### Two cogs connected by a chain of any length

O===*
SR = SL

### Two cogs connected by an axle of any length

O---*
SR = SL * TR / TL

Any invalid arrangement can throw an error or return 0.

## Test Cases

arrangement     O   o   *  speed  =>  speed

*-Oo-O         50  30  10    3.0  =>  -25.0
o*-O*==oO      50  30  10   -3.0  =>   15.0
O---*oO=O*-oo  15  12   8   10.0  =>    8.0
=o*            50  30  10   10.0  =>  error
o*-            50  30  10   10.0  =>  error
o-=*           50  30  10   10.0  =>  error
o *            50  30  10   10.0  =>  error


# Javascript – animated version

Playing on with canvas again. Adding a sample going far out of the scope of the task. But, anyhow, here it is. It is an animated / paint version.

### Issues

Can be a resource hog. In some quick tests it runs best in Chrome, worse in Opera and worst in Firefox. There is also an issue with movement as on relatively high speeds as the turn on each frame gets too big.

If one use small value for Teeth Width, it performs OK in various cases. Decor also has an impact.

By choice the use of Animation Frame or interval can be selected, and while using interval frames per second (FPS) can be tweaked. FPS can easily be lower then set if painting one frame takes longer.

If it only hogs, one can also use it to simply paint still images. Press Stop button and enter string to be parsed. With full decor it can be for a nice picture ;)

Try it out in this Fiddle.

Javascript code:

It could definitively have been tidied up some more and refactored.

requestAnimationFrame polyfill removed in this post due to limit of body length on SO.

var PI  = Math.PI,
P2  = PI*2,
cos = Math.cos,
sin = Math.sin;

function isNumber(n) {
return !isNaN(parseFloat(n)) && isFinite(n);
}

function isNumbers(a) {
var i;
for (i = 0; i < a.length; ++i)
if (!isNumber(a[i]))
return 0;
return 1;
}

function CogWheels() {
this.can    = document.getElementById('can');
this.ctx    = this.can.getContext("2d");
this.cm = {
main:{can: document.createElement('CANVAS'),ok:0}
//gear:{can: document.createElement('CANVAS'),ok:0}
};
this.cm.main.ctx = this.cm.main.can.getContext("2d");

this.ofs    = {x:10, y:50};
this.grect  = null;

this.parts  = [];
this.pix    = {wheel:[],axle:[],chain:[]};

this.sz     = {rb:100, rm:75, rs:50};
this.dir    = 1;
this.last   = null;
this.tw     = 4;
this.tl     = this.tw + 1;
this.navcr  = 7.5;

this.tsp    = null; // Timestamp previus frame
this.raf    = 0;    // Use requestAnimationFrame
this.mode   = 0;    // 1=Auto start
this.decor  = 3;    // Decoration level
this.pin    = 1;    // Paint "pin"
this.clr = {
wheel: '#321',
axle : '#852',
chain: '#842',
axis : '#a50',
teeth: '#1a1108',
pin  : '#f00',
dec1 : '#321'
};
}

var CW = CogWheels.prototype;

return {
c:this.ctx.shadowColor.match(/$$(.*)$$/)[1]
};
};

this.ctx.shadowColor    = p.c ? "rgba("+p.c+")" : "rgba(0,0,0,.8)";
};

};

/*
* Drawings for static buffer.
*
* * */
CW.drawBulb = function(x, y, r) {
this.ctx.globalCompositeOperation = 'source-over';
this.ctx.fillRect(x - r, y - r, x + r, y + r);
this.ctx.fillStyle = "#000";
};

CW.drawWheel = function(p) {
this.ctx.save();
this.ctx.globalCompositeOperation = 'source-over';
this.ctx.lineWidth      = this.tw;
this.ctx.strokeStyle    = this.clr.wheel;
this.ctx.moveTo(p.a[0], p.a[1]);
this.ctx.beginPath();
this.ctx.arc(p.a[0], p.a[1], p.r, 0, P2, p.d);
this.ctx.stroke();
if (this.decor > 0 && p.r > 10) {
this.ctx.beginPath();
this.ctx.strokeStyle = '#444';
this.ctx.lineWidth = 1;//0.8;
this.ctx.arc(p.a[0], p.a[1], p.r / 1.15, 0, P2, p.d === 1);
this.ctx.stroke();
}

// Axis
this.ctx.beginPath();
this.ctx.globalCompositeOperation = 'xor';
this.ctx.lineWidth = 1.5;
this.ctx.strokeStyle = this.clr.axis;
this.ctx.arc(p.a[0], p.a[1], this.navcr, 0, P2);
this.ctx.stroke();
this.ctx.restore();
//this.ctx.globalCompositeOperation = 'source-over';
};

CW.drawLinep = function(a) {
this.ctx.beginPath();
this.ctx.moveTo(a[0][0], a[0][1]);
this.ctx.lineTo(a[0][2], a[0][3]);
this.ctx.moveTo(a[1][0], a[1][1]);
this.ctx.lineTo(a[1][2], a[1][3]);
this.ctx.stroke();
};

CW.drawAxle = function(p) {
this.ctx.save();
this.ctx.globalCompositeOperation = 'source-over';
this.ctx.lineWidth = 2;
this.ctx.strokeStyle = this.clr.axle;

this.drawLinep(p.a);

/* Nav left wheel. */
this.ctx.beginPath();
this.ctx.lineWidth = 3;
this.ctx.globalCompositeOperation = 'xor';
this.ctx.arc(p.a[0][0], p.pp[1], this.navcr, 0, P2);
this.ctx.stroke();

/* Nav right wheel. */
this.ctx.beginPath();
this.ctx.arc(p.a[0][2], p.pp[1], this.navcr, 0, P2);
this.ctx.stroke();

this.ctx.restore();
};

CW.drawChain = function(p) {
this.ctx.save();
this.ctx.lineWidth = 2;
this.ctx.strokeStyle = this.clr.chain;
this.ctx.globalCompositeOperation = 'destination-over';

this.drawLinep(p.a);

this.ctx.restore();
};

CW.circleTangents = function(x1, y1, r1, x2, y2, r2) {
var d = Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)),
vx = (x2 - x1) / d,
vy = (y2 - y1) / d,
s1, s2, c, h,
nx, ny, a, re = [], i = 0;
for (s1 = 1; s1 >= -1; s1 -= 2) {
c = (r1 - s1 * r2) / d;
if (c*c > 1.0)
continue;
h = Math.sqrt(Math.max(0.0, 1.0 - c * c));
for (s2 = 1; s2 >= -1; s2 -= 2) {
nx = vx * c - s2 * h * vy;
ny = vy * c + s2 * h * vx;
a = [];
a[0] = x1 + r1 * nx;
a[1] = y1 + r1 * ny;
a[2] = x2 + s1 * r2 * nx;
a[3] = y2 + s1 * r2 * ny;
re[i++] = a;
}
}
return re;
};

/*
* Paint gear base to static buffer.
*
* */
CW.drawStatic = function() {
var sh;

if (this.cm.main.ok) {
this.ctx.drawImage(
this.cm.main.can,
this.cm.main.x,
this.cm.main.y
);
return;
}
};

CW.genStatic = function() {
var i, e,
p   = this.parts,
ix  = this.pix;

this.ctx.save();
if (this.shwheel) {
for (i = 0; i < ix.wheel.length; ++i) {
e = p[ix.wheel[i]];
this.drawBulb(e.a[0], e.a[1], e.r);
}
}
for (i = 0; i < ix.wheel.length; ++i) {
this.drawWheel(p[ix.wheel[i]]);
//setTimeout(this.drawWheel.bind(this, p[ix.wheel[i]]), (tt+=500));
}
for (i = 0; i < ix.axle.length; ++i) {
this.drawAxle(p[ix.axle[i]]);
//setTimeout(this.drawAxle.bind(this, p[ix.axle[i]]), (tt+=500));
}
for (i = 0; i < ix.chain.length; ++i) {
this.drawChain(p[ix.chain[i]]);
//setTimeout(this.drawChain.bind(this, p[ix.chain[i]]), (tt+=500));
}
for (i = 0; i < p.length; ++i) {
this.ctx.globalCompositeOperation = 'source-over';
this.ctx.strokeSyle = "#000";
this.ctx.font="10px sans-serif";
if (this.navcr > 10)
this.ctx.fillText(i, p[i].pp[0], p[i].pp[1]);
else if (p[i].w === 'wheel')
this.ctx.fillText('x', p[i].pp[0], p[i].pp[1]);
}
this.cm.main.can.width  = this.grect.w - this.ofs.x + 20;
this.cm.main.can.height = this.br * 2 + 40;
// Could have painted directly on memory canvas, but due to coordinates
// and post-implementation of memory canvas, it is here for now.
this.cm.main.ctx.drawImage(
this.can,                       // image
0,                              // dx
0,                              // dy
this.grect.w - this.ofs.x,      // dw
this.br * 2 + 60,               // dh WHY 60 HERE ????
-this.ofs.x + 20,               // sx
-this.ofs.y + 20,               // sy
this.grect.w - this.ofs.x ,     // sw
this.br * 2 + 60                // sh WHY 60 HERE ????
);
this.cm.main.x = this.ofs.x - 20;
this.cm.main.y = this.ofs.y - 20;
this.cm.main.ok = 1;
this.ctx.restore();
};

/*
* Drawings for moving parts.
*
* * */
CW.spikes = function(x1, y1, spikes, r0, r1, skew_0, skew_1, con) {
var t = PI / 2 * 3,
x = x1,
y = y1,
step = PI / spikes,
i, r2 = con ? r1 - 10 : r1;

this.ctx.beginPath();
this.ctx.moveTo(x1, y1 - r0);
for (i = 0; i < spikes; ++i) {
x = x1 + cos(t)*r0;
y = y1 + sin(t)*r0;
this.ctx.lineTo(x, y);
if (skew_0) {
t += step / 2;
x = x1 + cos(t)*r0;
y = y1 + sin(t)*r0;
this.ctx.lineTo(x, y);
t += (step - step/2);
} else {
t += step;
}

if (skew_1) {
x = x1 + cos(t - step/18) * r2;
y = y1 + sin(t - step/18) * r2;
this.ctx.lineTo(x, y);
if (con) {
x = x1 + cos(t - step/8) * r1;
y = y1 + sin(t - step/8) * r1;
this.ctx.lineTo(x, y);
x = x1 + cos(t + step/8) * r1;
y = y1 + sin(t + step/8) * r1;
this.ctx.lineTo(x, y);
}
x = x1 + cos(t + step/18) * r2;
y = y1 + sin(t + step/18) * r2;
this.ctx.lineTo(x, y);
} else {
x = x1 + cos(t) * r1;
y = y1 + sin(t) * r1;
this.ctx.lineTo(x, y);
}

if (skew_0) {
t += step / 2;
x = x1 + cos(t)*r0;
y = y1 + sin(t)*r0;
this.ctx.lineTo(x, y);
t += step - step/2;
} else {
t += step;
}
}
this.ctx.lineTo(x1, y1 - r0);
this.ctx.stroke();
this.ctx.closePath();
};

CW.drawTeeth = function(p) {
var i, th = p.h, x, y;
for (i = 0; i < p.t; ++i) {
x = p.pp[0] + p.r * cos(th);
y = p.pp[1] + p.r * sin(th);
this.ctx.moveTo(x, y);
this.ctx.lineTo(x + cos(th) * this.tl, y + sin(th) * this.tl);
th += p.thi;
}
return th;
};

CW.drawPin = function(p) {
var x = p.pp[0] + p.r * cos(p.h),
y = p.pp[1] + p.r * sin(p.h);
this.ctx.moveTo(
p.pp[0] + (this.navcr) * cos(p.h),
p.pp[1] + (this.navcr) * sin(p.h)
);
this.ctx.lineTo(x + cos(p.h) * this.tl, y + sin(p.h) * this.tl);
};

CW.drawDecor1 = function(p) {
var i, x, y,
th = p.h,
dd = p.r / 1.15 * 0.63,
rr = (p.r / this.spk) * 1.125;

this.ctx.strokeStyle = this.clr.dec1;
this.ctx.lineWidth = 2;
this.ctx.globalCompositeOperation = 'source-over';

for (i = 0; i < this.spk; ++i) {
x = p.pp[0] + dd * cos(th);
y = p.pp[1] + dd * sin(th);
this.ctx.moveTo(x, y);
this.ctx.beginPath();
this.ctx.arc(x, y, rr, 0, P2);
this.ctx.stroke();
th += PI/this.spk*2;
}
//return th;
};

CW.drawDecor2 = function(p) {
var theta = p.h;
this.ctx.save();
this.ctx.translate(p.pp[0], p.pp[1]);
this.ctx.rotate(theta - (PI / 2 * 3));
this.ctx.lineWidth = 2;
if (this.star)
this.spikes(0, 0, this.spk, p.r / 6 /*this.navcr*/, p.r / 1.19);
else if (p.r > 50)
this.spikes(0, 0, this.spk, p.r / 6 /*this.navcr*/, p.r / 1.15, 1, 1, 1);
else
this.spikes(0, 0, this.spk, p.r / 6 /*this.navcr*/, p.r / 1.19, 1, 1);
this.ctx.restore();
};

CW.drawAnim = function(tt) {
var i, j,
p = this.parts,
ix = this.pix,

this.ctx.save();

if (this.decor > 1) {
for (i = 0; i < ix.wheel.length; ++i) {
if (p[ix.wheel[i]].r < this.navcr * 2)
continue;
if ((this.decor === 2 || this.decor > 3))
this.drawDecor1(p[ix.wheel[i]]);
if ((this.decor > 2))
this.drawDecor2(p[ix.wheel[i]]);
}
}

this.ctx.lineWidth = this.tw;

if (this.pin) {
this.ctx.beginPath();
this.ctx.strokeStyle = this.clr.pin;
this.ctx.globalCompositeOperation = 'darker';
for (i = 0; i < ix.wheel.length; ++i)
this.drawPin(p[ix.wheel[i]]);
this.ctx.stroke();
}

this.ctx.globalCompositeOperation = 'destination-over';
this.ctx.strokeStyle = this.clr.teeth;
this.ctx.beginPath();
for (i = 0; i < ix.wheel.length; ++i) {
j = ix.wheel[i];
this.drawTeeth(p[j]);
p[j].h += p[j].s * tt * p[j].d;
}
this.ctx.stroke();

this.ctx.restore();
};

/*
* Build functions.
*
* * */
var e = {
w: a.type,
s: a.speed,
d: this.dir,
h: a.theta,
};
if (a.type === 'wheel') {
if (this.last === 'wheel')
this.dir = -this.dir;
e.t     = a.teeth;
e.d     = this.dir;
// Theta increment
e.thi   = PI / a.teeth * 2;
} else if (a.type === 'axle' || a.type === 'chain') {
e.t     = 30;
} else {
// Throw error
return null;
}
e.hs = e.h; // Starting angle
this.last = a.type;
this.parts.push(e);
this.grect.w += e.r * 2 + this.tl * 2;
return this;
};

/*
* Various calculations that is the same for each frame.
*
* */
CW.postBuild = function() {
var i,
x1 = this.ofs.x, x2,
y1 = this.ofs.y + this.br, y2,
p  = this.parts,
c  = this.navcr,
a, d, txl = this.navcr > 10 ? 5 : 15,
tf = this.tw < 2 ? 2 : 3;

this.ctx.textBaseline = "middle";
this.ctx.fillText("tps:", 2, this.grect.h + txl);
txl += 20;
this.ctx.fillText("Hz:", 2, this.grect.h + txl);
txl -= 10;
this.ctx.textAlign = "center";

for (i = 0; i < p.length; ++i) {
if (p[i].w === 'wheel') {
x1 += p[i].r;
p[i].a = [x1, y1];
p[i].tps = (1 / ((P2 / p[i].t) / p[i].s) * p[i].d).toFixed(1);
if (this.navcr <= 10)
this.ctx.fillText(i, x1, this.grect.h + 5);
this.ctx.fillText(p[i].tps, x1, this.grect.h + txl);
this.ctx.fillText((Math.abs(p[i].tps)/p[i].t).toFixed(tf) ,
x1, this.grect.h + txl + 20);
this.pix.wheel.push(i);
} else if (p[i].w === 'axle') { // AXLE
x1 -= p[i-1].r + c;
x2 = x1 + p[i-1].r + c + p[i].r*2 + c + p[i + 1].r;
a = [];
// Left - right nav, top
a[0] = [x1, y1 - c, x2, y1 - c];
// Left - right nav, bottom
a[1] = [x1, y1 + c, x2, y1 + c];
p[i].a = a;
d = x2 - x1;
p[i].cc = [(x1+x2)/2, y1, d / 2];
p[i].t  = d * 2 / this.tw;
x1 += p[i - 1].r + c + p[i].r;
this.pix.axle.push(i);
} else if (p[i].w === 'chain') {  // CHAIN
x1 -= p[i-1].r + c;
x2 = x1 + p[i-1].r + c + p[i].r*2 + c + p[i + 1].r;
y2 = y1 - p[i+1].r - c ;
p[i].a = this.circleTangents(x1, y1, p[i-1].r, x2, y1, p[i+1].r);
x1 += p[i - 1].r + c + p[i].r;
this.pix.chain.push(i);
}
p[i].pp = [x1, y1];
x1 += p[i].r + c;
}
document.getElementById('tps_e').value = p[--i].tps;
};

/*
*
* @a   Arrangement
* @tb  Teeth O wheel
* @tm  Teeth o wheel
* @ts  Teeth * wheel
* @tps Teeth per second leftmost wheel
* */
CW.build = function(a, tb, tm, ts, tps) {
var i, w, c,
ob, om, os, // Odd/even flag
sp = null,  // Speed
pr = '',    // Previous
th,         // theta
hor = 0,    // If last wheel has right most tooth horizontal.
xcw;        // Axel / Chain base width.

if (a.match(/^[=-]|[=-]$|-=|=-/)) return 1; clearInterval(this.iv); this.tsp = null; this.parts = []; this.pix = {wheel:[],axle:[],chain:[]}; this.imgm = 0; //this.ctx.clearRect(0, 0, this.can.width, this.can.height); this.can.width = this.can.width; this.spk = parseInt(document.getElementById('spikes').value); this.tw = parseFloat(document.getElementById('teeth_w').value); if (!isNumbers([tb, tm, ts, tps, this.tw, this.spk])) return 1; if (tb < 1 || tm < 1 || ts < 1 /*|| tps === 0*/ || this.tw <= 0) return 1; this.tl = this.tw + 2; this.navcr = this.tl * 1.5; xcw = this.tl * 2; this.sz.rb = rb = this.tw * tb / PI; this.sz.rm = rm = this.tw * tm / PI; this.sz.rs = rs = this.tw * ts / PI; // Biggest radius this.br = rb > rm ? rb : rm; if (rs > this.br) this.br = rs; ob = tb % 2; om = tm % 2; os = ts % 2; // Offset x:left, y:center first wheel this.ofs = { x: this.tl * 3, y: /*this.br +*/ 50 }; // Repaint rectangle this.grect = { w: this.ofs.x, h: this.br * 2 + 100 }; this.dir = tps > 0 ? 1 : -1; tps = Math.abs(tps); this.last = null; for (i = 0; i < a.length; ++i) { th = 0; switch ((c = a.charAt(i))) { case 'O': if (sp === null) { sp = (P2 / tb) * tps; } else if (pr === 'o') { sp = sp / rb * rm; } else if (pr === '*') { sp = sp / rb * rs; } // ODD + HOR = no skew // If odd result is skew if ((hor && ob) || (!hor && !ob)) { hor = 1; } else { th = this.tw / rb; hor = 0; } this.add({type:'wheel', rad:rb, teeth:tb, speed:sp, theta:th}); break; case 'o': if (sp === null) { sp = (P2 / tm) * tps; } if (pr === 'O') { sp = sp / rm * rb; } else if (pr === '*') { sp = sp / rm * rs; } if ((hor && om) || (!hor && !om)) { hor = 1; } else { th = this.tw / rm; hor = 0; } this.add({type:'wheel', rad:rm, teeth:tm, speed:sp, theta:th}); break; case '*': if (sp === null) { sp = (P2 / ts) * tps; } if (pr === 'O') { sp = sp / rs * rb; } else if (pr === 'o') { sp = sp / rs * rm; } if ((hor && os) || (!hor && !os)) { hor = 1; } else { th = this.tw / rs; hor = 0; } this.add({type:'wheel', rad:rs, teeth:ts, speed:sp, theta:th}); break; case '-': for (w = 1; a.charAt(i + 1) === '-'; ++i, ++w) ; this.add({type:'axle', rad:w * xcw, speed:sp, theta:th}); hor = 1; break; case '=': for (w = 1; a.charAt(i + 1) === '='; ++i, ++w) ; this.add({type:'chain', rad: w * xcw, speed:sp, theta:th}); hor = 1; break; default: this.parts = []; return 1; } if (c !== '=') pr = a.charAt(i); } if (!this.parts.length) return 1; // Hack to prevent stray borders in page. this.can.style.display = 'none'; this.can.width = this.can.width + 50; this.can.width = this.grect.w; this.can.height = this.grect.h + 50; this.can.style.display = 'block'; this.postBuild(); if (this.shade) { //this.setShadow({x:-1,y:1,b:13}); this.setShadow({c:"0,0,0,.6", x:-4, y:4, b:13}); this.setShadow({c:"0,0,0,.8", x:0, y:0, b:this.tw*2}); //this.ctx.shadowBlur = 17; } else { this.ctx.shadowBlur = 0; } this.genStatic(); this.step(); }; /* * Runtime functions. * * * */ CW.tick = function(tt) { this.ctx.clearRect(0, 0, this.grect.w, this.grect.h); this.noShadow(); this.ctx.textAlign = "left"; this.ctx.fillText("F: " + tt, 15, 15); this.ctx.textAlign = "center"; if (this.shade) { this.setShadow({c:"0,0,0,.8",x:0,y:0,b:this.tw*2}); } this.drawStatic(); this.drawAnim(tt); }; CW.step = function () { if (this.parts.length) this.tick((1/this.fps).toFixed(3)); }; CW.run_raf = function(ts) { if (this.tsp === null) { this.tsp = ts; } this.tsp = ts - this.tsp; this.tick(this.tsp/1000); requestAnimationFrame(this.run.bind(this)); this.tsp = ts; }; CW.run_iv = function() { var ts, that = this, ic = this.fps; this.tsp = +Date.now(); this.iv = setInterval(function() { ts = +Date.now() - that.tsp; that.tsp = +Date.now(); that.tick(ts/1000); if (ic !== that.fps) { clearInterval(that.iv); that.run_iv(); } }, 1000 / this.fps); }; CW.stop = function () { clearInterval(this.iv); this.mode = 0; }; CW.run = function () { this.stop(); if (!this.parts.length) return this.ui_build_run; this.mode = 1; this.raf = !document.getElementById('run_iv').checked; if (this.raf) requestAnimationFrame(this.run_raf.bind(this)); else this.run_iv(); }; /* * UI functions * * * */ CW.ui_build = function() { if (this.build( document.getElementById('arr').value, +document.getElementById('teeth_b').value, +document.getElementById('teeth_m').value, +document.getElementById('teeth_s').value, +document.getElementById('tps_1').value )) return 1; }; CW.ui_build_run = function() { if (this.ui_build()) return 1; if (this.mode > 0) this.run(); }; /* * This should have been refactored. * * */ CW.ui_init = function() { var i, cw = this, e = document.getElementById('fps'), fv = document.getElementById('fpsv'), dv = document.getElementById('decorv'); this.fps = +e.value; fv.innerHTML = this.fps.toFixed(1); e.addEventListener('change', function(e) { cw.fps = parseFloat(e.target.value); fv.innerHTML = cw.fps.toFixed(1); }); e = document.getElementById('decor'); this.decor = +e.value; dv.innerHTML = this.decor; e.addEventListener('change', function(e) { cw.decor = parseInt(e.target.value); dv.innerHTML = cw.decor; cw.ui_build_run(); }); e = document.getElementById('pin'); this.pin = !!e.checked; e.addEventListener('change', function(e) { cw.pin = e.target.checked; cw.ui_build_run(); }); e = document.getElementById('shadow'); this.shade = !!e.checked; e.addEventListener('change', function(e) { cw.shade = e.target.checked; cw.ui_build_run(); }); e = document.getElementById('shwheel'); this.shwheel = !!e.checked; e.addEventListener('change', function(e) { cw.shwheel = e.target.checked; cw.ui_build_run(); }); e = document.getElementById('star'); this.star = !!e.checked; e.addEventListener('change', function(e) { cw.star = e.target.checked; cw.ui_build_run(); }); e = document.getElementById('run_iv'); cw.raf = !e.checked; document.getElementById('build').addEventListener( 'click', cw.ui_build.bind(cw)); document.getElementById('stop').addEventListener( 'click', cw.stop.bind(cw)); document.getElementById('step').addEventListener( 'click', cw.step.bind(cw)); document.getElementById('run').addEventListener( 'click', cw.run.bind(cw)); e = document.getElementsByTagName('INPUT'); for (i = 0; i < e.length; ++i) { if (e[i].type === 'text') { e[i].addEventListener('keyup', cw.ui_build_run.bind(cw)); } else if (e[i].type === 'radio') { e[i].addEventListener('change', cw.ui_build_run.bind(cw)); } } }; document.addEventListener("DOMContentLoaded", function () { var cw = new CogWheels(); cw.ui_init(); cw.ui_build_run(); });  # Julia chain(s) = all((c)->c=='=',s) axle(s) = all((c)->c=='-',s) direct(s) = s=="" function mechanical(arrangment,O,o,x,speed_l) teeth = {"O"=>O, "o"=>o, "*"=>x} process(c) = direct(c[2]) ? -1 : chain(c[2]) ? 1 : axle(c[2]) ? teeth[c[3]]/teeth[c[1]] : 0 matches = collect(eachmatch(r"([Oo*])([-=]*)([Oo*])",arrangment,true)) length(matches) == 0 ? 0 : speed_l*prod([process(m.captures) for m in matches]) end # test cases println(mechanical("*-Oo-O",50,30,10,3)) println(mechanical("o*-O*= =oO",50,30,10,-3)) println(mechanical("O---*oO=O*-oo",15,12,8,10)) println(mechanical("O *",50,30,10,10))  Gives correct output. r"stuff" is a regex. # Java import java.util.Scanner; /** * * @author Quincunx */ public class MechanicalCoding { final static int[] teeth = new int[3]; public static void main(String[] args) { Scanner scan = new Scanner(System.in); System.out.println("Input the arrangement:"); String layout = scan.nextLine(); System.out.println("Input the:"); System.out.print("\tNumber of teeth on large gear:\n\t"); teeth[2] = scan.nextInt(); scan.nextLine(); System.out.print("\tNumber of teeth on medium gear:\n\t"); teeth[1] = scan.nextInt(); scan.nextLine(); System.out.print("\tNumber of teeth on small gear:\n\t"); teeth[0] = scan.nextInt(); scan.nextLine(); System.out.println("Input the speed of the left hand side:"); double speed = scan.nextDouble(); layout = layout.replaceAll("([-=])+", "$1");

char lastC = ' ';
boolean mustBeNext = false;
for (char c : layout.toCharArray()){
switch (c){
case 'O':
case 'o':
case '*':
if (lastC == '=' || lastC == ' '){
break;
}
if (lastC == '-'){
speed *= 1.*teeth(c)/teeth(lastC);
break;
}
speed *= -1;
break;
case '-':
case '=':
default:
if (lastC == ' '){
System.exit(1);
}
}
lastC = c;
}
if (lastC == '-' || lastC == '='){
System.exit(1);
}
System.out.println(speed);
}
public static int teeth(char c){
return teeth[c=='O'?2:c=='o'?1:0];
}
}


What a relief to be able to use Java! Sample run:

Input the arrangement:
o*-O*==oO
Input the:
Number of teeth on large gear:
50
Number of teeth on medium gear:
30
Number of teeth on small gear:
10
Input the speed of the left hand side:
-3.0
15.0


## C#, 396 bytes

Golfed for fun with added indentation. I'm sure the same code would be smaller in a loosely-typed language.

namespace System.Text.RegularExpressions{
class P{
static void Main(string[]a){
var s=Regex.IsMatch(a[0],"^[-=]|[-=]\$|-=|=-|[^-=Oo*]")?0:double.Parse(a[4]);
foreach(Match m in Regex.Matches(a[0],"[Oo*]{2,}"))
s*=m.Length%2*2-1;
foreach(Match m in Regex.Matches(a[0],"[Oo*]-+[Oo*]"))
s*=double.Parse(a[" Oo*".IndexOf(m.Value[m.Length-1])])/double.Parse(a[" Oo*".IndexOf(m.Value[0])]);
Console.Write(s);
}
}
}


# 159 Bytes - Javascript

This uses ECMAScript 6 Features so will only work in browsers that support those - Firefox is one.

(g,l,m,s,v,n)=>{for(a={'O':l,'o':m,'*':s};n!=g&&(n=g);g=g.replace(/[Oo*](-*|=*)([Oo*])/,(w,y,z)=>((v*=y?(y[0]=='='?1:a[z]/a[w[0]]):-1)-v)||z));return a[g]?v:0}


Defines an anonymous function that takes the parameters arrangement O o * speed and returns the speed of the last cog for a valid arrangement, and zero otherwise.

Can test all test cases in firefox with:

[((g,l,m,s,v,n)=>{for(a={'O':l,'o':m,'*':s};n!=g&&(n=g);g=g.replace(/[Oo*](-*|=*)([Oo*])/,(w,y,z)=>((v*=y?(y[0]=='='?1:a[z]/a[w[0]]):-1)-v)||z));return a[g]?v:0}).apply(0,a) for each (a in [['*-Oo-O',50,30,10,3],['o*-O*==oO',50,30,10,-3],['O---*oO=O*-oo',15,12,8,10],['=o*',50,30,10,-3],['o*-',50,30,10,-3],['o-=*',50,30,10,-3],['o *',50,30,10,-3]])]