Greasy Fork is available in English.

Amazon - Prix au kilo

Affiche les prix au kilo et tri par prix croissant

// ==UserScript==
// @name         Amazon - Prix au kilo
// @name:en      Amazon - Price per weight
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  Affiche les prix au kilo et tri par prix croissant
// @description:en Display price per weight & sort ascending
// @author       Shuunen
// @match        https://*.amazon.fr/*
// @grant        none
// ==/UserScript==

(function() {
	"use strict";

	// Used https://babeljs.io/repl/ to convert this code to es2015

	var app = {
	  id: "amz-kg",
	  processOne: false,
	  processOnce: false,
	  hideStuff: false,
	  showDebug: false,
	  injectRealPrice: true,
	  sortProducts: true
	};

	app.debug = app.processOne;

	var cls = {
	  handled: app.id + "-handled",
	  avoided: app.id + "-avoided",
	  debug: app.id + "-debug",
	  pricePer: app.id + "-price-per"
	};

	var selectors = {
	  list: "ul.s-result-list",
	  item:
		".s-result-item:not(.aok-hidden):not(." +
		cls.handled +
		"):not(." +
		cls.avoided +
		")",
	  itemTitle: ".s-access-title",
	  otherPrice: ".a-size-base.a-color-price.s-price.a-text-bold",
	  pricePer:
		".a-size-base.a-color-price:not(.s-price):not(.a-text-bold)," +
		"." +
		cls.pricePer,
	  debugContainer:
		".a-fixed-left-grid-col.a-col-right .a-row:not(.a-spacing-small) .a-column.a-span7",
	  debug: "." + cls.debug,
	  pantry: "img.sprPantry",
	  stuffToHide: ".s-result-item .a-column.a-span7 .a-row:not(." + cls.debug + ")"
	};

	selectors.price = selectors.debugContainer + " div:first-child .a-link-normal";

	var regex = {
	  price: /EUR (\d+,\d\d)/i,
	  weight: /(\d+)\s?(g|-)/i,
	  bulk: /Lot de (\d+)/i,
	  pricePer: /EUR (\d+,\d\d)\/(\w+)(\s\w+)?/i
	};

	var templates = {
	  debug:
		'<div class="a-row ' +
		cls.debug +
		'"><div class="a-column a-span12">\n    <p class="a-spacing-micro">Price  : {{price}} \u20AC</p>\n    <p class="a-spacing-micro">Weight : {{weight}} {{unit}}</p>    \n    <p class="a-spacing-small">Bulk   : {{bulk}}</p>\n    <p class="a-spacing-micro a-size-base a-color-price s-price a-text-bold">P/Kg  : {{pricePerKilo}} \u20AC/kg</p>\n    </div></div>',
	  price: '<span class="s-price a-text-bold">EUR {{price}}</span>',
	  pricePerKilo:
		'<span class="a-color-price s-price a-text-bold ' +
		cls.pricePer +
		'">EUR {{pricePerKilo}}/kg</span>'
	};

	var products = [];

	function findOne(selector, context, dontYell) {
	  context = context || document;
	  var item = context.querySelector(selector);
	  if (item && app.debug) {
		console.log('found element matching "' + selector + '"');
	  } else if (!item && !dontYell) {
		console.warn('found no element for selector "' + selector + '"');
	  }
	  return item;
	}

	function findFirst(selector, context) {
	  return findAll(selector, context)[0];
	}

	function findAll(selector, context) {
	  context = context || document;
	  var items = Array.prototype.slice.call(context.querySelectorAll(selector));
	  if (items.length && app.debug) {
		console.log("found", items.length, 'elements matching "' + selector + '"');
	  } else if (!items.length) {
		console.warn('found no elements for selector "' + selector + '"');
	  }
	  return items;
	}

	function shadeBadProducts() {
	  findAll(selectors.pantry).forEach(function(el) {
		var item =
		  el.parentElement.parentElement.parentElement.parentElement.parentElement
			.parentElement.parentElement.parentElement;
		item.style.filter = "grayscale(100%)";
		item.style.opacity = 0.5;
		item.style.order = 1000;
		item.classList.add(cls.avoided);
		if (app.debug) {
		  console.log("shaded item", item);
		}
	  });
	}

	function priceStrToFloat(str) {
	  var price = str.replace(",", ".");
	  price = parseFloat(price);
	  return price;
	}

	function priceFloatToStr(num) {
	  var price = num.toFixed(1);
	  price = price.replace(".", ",") + "0";
	  return price;
	}

	function getPrice(text) {
	  var matches = text.match(regex.price);
	  if (app.debug) {
		console.log("found price matches :", matches);
	  }
	  var price = matches && matches.length === 2 ? matches[1] : "0";
	  price = priceStrToFloat(price);
	  if (app.debug) {
		console.log("found price", price);
	  }
	  return price;
	}

	function getWeightAndUnit(text) {
	  var matches = text.match(regex.weight);
	  // console.log('found weight matches & unit :', matches)
	  var data = {
		weight: 0,
		unit: ""
	  };
	  if (matches && matches.length === 3) {
		data.weight = matches[1];
		data.unit = matches[2];
	  }
	  if (data.unit === "-") {
		data.unit = "g";
	  }
	  // console.log('found weight & unit :', data)
	  return data;
	}

	function getBulk(text) {
	  var matches = text.match(regex.bulk);
	  // console.log('found bulk matches :', matches)
	  var bulk = matches && matches.length === 2 ? matches[1] : "1";
	  bulk = parseInt(bulk);
	  // console.log('found bulk', bulk)
	  return bulk;
	}

	function getProductDataViaPricePer(text) {
	  var matches = text.match(regex.pricePer);
	  // console.log('found pricePer matches :', matches)
	  var data = {
		price: 0,
		weight: 0,
		unit: "",
		bulk: 1
	  };
	  if (matches && matches.length === 4) {
		data.price = priceStrToFloat(matches[1]);
		if (matches[3]) {
		  data.weight = matches[2];
		  data.unit = matches[3].trim();
		} else {
		  data.weight = 1;
		  data.unit = matches[2];
		}
	  }
	  if (app.debug) {
		console.log("found pricePer :", data);
	  }
	  return data;
	}

	function getTitle(text) {
	  return text
		.split(" ")
		.slice(0, 5)
		.join(" ");
	}

	function getProductData(item, data) {
	  var text = item.textContent;
	  var weightAndUnit = getWeightAndUnit(text);
	  data = data || {};
	  data.price = getPrice(text);
	  data.weight = weightAndUnit.weight;
	  data.unit = weightAndUnit.unit;
	  data.bulk = getBulk(text);
	  data.title = getTitle(text);
	  return data;
	}

	function fill(template, data) {
	  var tpl = template + "";
	  Object.keys(data).forEach(function(key) {
		var str = "{{" + key + "}}";
		var val = data[key];
		if (key.indexOf("price") > -1 && val > 0) {
		  val = priceFloatToStr(val);
		}
		// console.log('looking for', str)
		tpl = tpl.replace(new RegExp(str, "gi"), val);
	  });
	  return tpl;
	}

	function showDebugData(item, data) {
	  var debug = findOne(selectors.debug, item, true);
	  if (!app.showDebug) {
		if (debug) {
		  // if existing debug zone found
		  debug.style.display = "none";
		}
		return;
	  }
	  var html = fill(templates.debug, data);
	  // console.log('debug html', html)
	  if (debug) {
		// if existing debug zone found
		debug.style.display = "inherit";
		debug.outerHTML = html;
		return;
	  }
	  debug = document.createElement("div");
	  debug.innerHTML = html;
	  var container = findOne(selectors.debugContainer, item);
	  if (container) {
		container.append(debug);
	  } else {
		console.error(data.title, ": failed at finding debug container", item);
	  }
	}

	function getPricePerKilo(data) {
	  data.pricePerKilo = 0;
	  if (data.weight === 0) {
		return data;
	  }
	  var w = data.weight * data.bulk;
	  if (data.unit === "g") {
		data.pricePerKilo = (1000 / w) * data.price;
	  } else if (data.unit === "kg") {
		data.pricePerKilo = w * data.price;
	  } else {
		console.error(data.title, ": unit not handled :", data.unit);
	  }
	  if (data.pricePerKilo >= 0) {
		data.pricePerKilo = priceStrToFloat(data.pricePerKilo.toFixed(1));
	  }
	  if (app.debug) {
		console.log("found pricePerKilo :", data);
	  }
	  return data;
	}

	function injectRealPrice(item, data) {
	  if (!app.injectRealPrice) {
		return;
	  }
	  if (app.debug) {
		console.log("injecting real price :", data);
	  }
	  var price = findOne(selectors.price, item);
	  var text = "";
	  if (data.pricePerKilo > 0) {
		text = fill(templates.pricePerKilo, data);
	  } else if (data.price > 0) {
		text = fill(templates.price, data);
	  }
	  var pricePer = findOne(selectors.pricePer, item, true);
	  if (pricePer) {
		pricePer.style.display = "none";
	  }
	  price.innerHTML = text;
	  var otherPrice = findOne(selectors.otherPrice, item, true);
	  if (otherPrice) {
		otherPrice.classList.remove("a-color-price", "a-text-bold");
	  }
	}

	function avoidProduct(item) {
	  var nbAttr = item.getAttributeNames().length;
	  if (nbAttr === 5) {
		if (app.debug) {
		  console.warn("detected ad product", item);
		}
		return true;
	  }
	  // all good
	  return false;
	}

	function augmentProducts() {
	  findAll(selectors.item).forEach(function(item) {
		return augmentProduct(item);
	  });
	}

	function augmentProduct(item) {
	  if (avoidProduct(item)) {
		return;
	  }
	  var pricePer = findOne(selectors.pricePer, item, true);
      var data = {};
	  if (pricePer) {
		pricePer.style.display = "inherit";
		data = getProductDataViaPricePer(pricePer.textContent);
	  } else {
		data = getProductData(item);
	  }
	  data = getPricePerKilo(data);
	  if (pricePer) {
		data = getProductData(item, data);
	  }
	  showDebugData(item, data);
	  injectRealPrice(item, data);
	  if (app.processOnce) {
		item.classList.add(cls.handled);
	  }
	  data.el = item;
	  products.push(data);
	}

	function hideStuff() {
	  findAll(selectors.stuffToHide).forEach(function(el) {
		return (el.style.display = app.hideStuff ? "none" : "inherit");
	  });
	}

	function sortProducts() {
	  var list = findOne(selectors.list);
	  if (!list) {
		return console.error("cannot sort without list");
	  }
	  list.style.display = "flex";
	  list.style.flexDirection = "column";
	  // trick to have products without pricePerKilo at bottom
	  products.map(function(p) {
		return (p.pricePerKilo = p.pricePerKilo || p.price + 1000);
	  });
	  // sort by pricePerKilo
	  products = products.sort(function(a, b) {
		return a.pricePerKilo - b.pricePerKilo;
	  });
	  products.forEach(function(p, i) {
		return (p.el.style.order = i);
	  });
	}

	function init() {
	  console.log(app.id, "is starting...");
	  shadeBadProducts();
	  if (app.processOne) {
		augmentProduct(findFirst(selectors.item));
	  } else {
		augmentProducts();
		sortProducts();
	  }
	  hideStuff();
	  console.log(app.id, "processed", products.length, "products");
	}

	init();

})();