/* * [Randomiser.java] * * Summary: Select a "random" quotation from a collection of quotations. * * Copyright: (c) 2009-2017 Roedy Green, Canadian Mind Products, http://mindprod.com * * Licence: This software may be copied and used freely for any purpose but military. * http://mindprod.com/contact/nonmil.html * * Requires: JDK 1.8+ * * Created with: JetBrains IntelliJ IDEA IDE http://www.jetbrains.com/idea/ * * Version History: * 1.8 2009-02-06 include go package in ZIP bundle. * will be inserted, and where on the page it will be inserted. * this is not a Macro. It is a collection of methods to help select random quotations, * random advertisement, * PSAs, that change periodically. The selections are not random. They produce the same results every * time * the code is run. This allows choices to remain stable for weeks at a time. There is nothing * specific to * ads or quotations in this class. */ package com.mindprod.htmlmacros.support; import com.mindprod.common18.FNV1a64Digester; import java.io.File; import static java.lang.System.*; /** * Select a "random" quotation from a collection of quotations. *

* Selection is based on the time and the digest of the name of the file where the quotation is being inserted and * where on the page it is being inserted. *

* It is not really random. It is completely reproducible. *

* This is not a Macro. It is a collection of methods to help select random quotations, random advertisement, * PSAs, that change periodically. The selections are not random. They produce the same results every time * the code is run. This allows choices to remain stable for weeks at a time. There is nothing specific to * ads or quotations in this class. * * @author Roedy Green, Canadian Mind Products * @version 1.8 2009-02-06 include go package in ZIP bundle. * @since 2009 */ class Randomiser { // todo: convert to enum // declarations private static final boolean DEBUGGING = false; /** * which invocation on the page is this, 0 is first 1 is second etc. */ private static int sequenceOnPage = 0; /** * previous fileBeingProcessed */ private static File prevFileBeingProcessed = null; // /declarations // methods /** * Generate a 32 bit random digest number that roughly speaking depends on letters in file name, * where we are on page, current time, changeIntervall. So it will stay stable for a while. * * @param changeIntervalInMillis how often to change the ad or quotation in millis. * @param fileBeingProcessed the file currently being processed. * * @return random 32-bit number that changes when the file name changes, and changes over time, * once every changeIntervalInMillis * but usually remains stable. */ public static int getHashForFilename( final long changeIntervalInMillis, final File fileBeingProcessed ) { // Why is this so complicated? // 1. we want to change psa ads, quotations, non-mil etc on a // regular basis, some pages more frequently. // 2. we don't want all pages changing at once triggering big uploads, so we stagger them. // 3. if the quotations change, even if the hash do does not change, the chosen quote // will change, triggering mass updates. So we choke this back by only updating // a random selection for each run using set resetfooterpercentage. if ( fileBeingProcessed.equals( prevFileBeingProcessed ) ) { sequenceOnPage++; } else { sequenceOnPage = 0; prevFileBeingProcessed = fileBeingProcessed; } // Used both for ads and quotations. Don't insert any code there specific to either. // We want to want to ensure each ad rotates at a given interval, but not all ads should // change on the same day. We want to make sure quotation at top and bottom of the page // change at the same time, but don't select the same quotation. // Use SHA-1 instead of CRC32 to get better scrambling. // We are not hashing the content of any page, just the file name. // throw in hashCode so all randoms won't change at once. // seed will change every changeIntervalInMillis. The value itself has no physical meaning. // if the changeInterval is greater than a day, we arrange things so that the change occurs at midnight // UTC, and // roughly 1/n of them change on any given day, where n is the change interval in days. // if the changeInterval is greater than an hour, we arrange things so that the change occurs on the hour // UTC, // roughly 1/n of them change on any given hour, where n in the change interval in hours. // we adjust the file timestamp by a stable random amount so all hashes will not change at once // for Quotations, this will be called twice, once to select the flock and once to select // the the quotation within the flock. final int fileHash = fileBeingProcessed.hashCode(); final long accelerateChangeByMillis = ( fileHash & Integer.MAX_VALUE ) % changeIntervalInMillis; // seed will change only once every changeIntervalMillis, but not all quotes/ads will change at once // because they have different accelerateChangeByMillis. // seed is like a timer that increments every changeInterval final long now = System.currentTimeMillis(); final int seed = ( int ) ( ( now + accelerateChangeByMillis ) / changeIntervalInMillis ); // hash 4-bytes of the seed in big-endian order. FNV1a64Digester digester = new FNV1a64Digester(); digester.updateInt( seed ); digester.updateInt( fileHash ); // hash in index on page. // only low bits will be occupied. digester.update( ( sequenceOnPage * 41 ) & 0xff ); // extract scrambled digest final long digest = digester.getValue(); // collapse 64-bit hash down to 32 final int hash = ( int ) ( ( digest >>> 32 ) ^ digest ); if ( DEBUGGING ) { out.println( "\nhash:" + hash + " fileHash:" + fileHash + " interval:" + changeIntervalInMillis + " seed:" + seed + " file:" + fileBeingProcessed.getAbsolutePath() + " seq:" + sequenceOnPage ); } return hash; } // /method /** * given a 32-bit hash/digest, returns an a selection number. * * @param hash 32 bits, possibly signed. * @param possibilities how many possible choices there are * * @return a repeatable number in the range 0..possibilities.-1, If possibilities is 0, returns -1. */ @SuppressWarnings( { "WeakerAccess" } ) public static int getRandomSelectorIndexForHash( final int hash, final int possibilities ) { if ( possibilities == 0 ) { return -1; } // strip sign bit and take modulus return ( hash & Integer.MAX_VALUE ) % possibilities; } // /method /** * given a 32-bit hash/digest, returns an a selection number. * * @param hash 32 bits, possibly signed. * @param weights weight for choices 0..n-1. Not necessarily normalised to 100. * * @return a repeatable number in the range 0..n-1 based on hash. */ @SuppressWarnings( { "WeakerAccess" } ) public static int getWeightedSelectorIndexForHash( final int hash, final int... weights ) { // we need to normalise the weights int sum1 = 0; for ( int weight : weights ) { sum1 += weight; } // generate a randomised number 0..sum1-1 final int cutoff = ( hash & Integer.MAX_VALUE ) % sum1; // if weights were 5, 10, 5, then sum is 20 and assignments // 0..4 -> 0 // 5..14 -> 1 // 15..19 -> 2 // code works even if some weights are zero. int sum2 = 0; for ( int choice = 0; choice < weights.length; choice++ ) { sum2 += weights[ choice ]; if ( sum2 > cutoff ) { return choice; } } assert false : "program should never get here"; return weights.length - 1; } // /method // /methods }