úterý 5. srpna 2008

Počítáme v Javě (od sčítání k nelineárnímu modelování)

Tento záznam měl původně obsahovat jen malý komentář ke knihovně Math z Apache commons, ale trochu se to psaní rozjelo. Doufám, že vás to neodradí.

Začínáme počítat

Většina programátorů v Javě asi umí pracovat s čísly a dokáže si dobře spočítat, co by měl jejich program dělat. Jestli to potom opravdu dělá je věc druhá. Člověk si při učení Javy většinou začíná hrát s jednoduchými operátory (+ - * / %) a primitivními typy (int, double), po čase začne zkracovat (++ *= ...), sem tam použije bitové operace (| & ~ << >> >>>) a už se mu zdá, že je za vodou. Ale pak narazí na problémy.

Další level

Třeba taková nevinná konstrukce dělení. Žádné neukončené desetiné rozvoje nebo nedejbože dělení nulou.

double ctvrtina = 1 / 4;
System.out.println(ctvrtina);  //0.0 WTF?
Stačí zapomenout, že výsledkem dělení celých čísel je opět celé číslo a máme oheň na střeše.

Další nástrahy se skrývají v číslech s plovoucí desetinou čárkou a s tím související nutností neustále zaokrouhlovat (např. když počítáte peníze na účtech pomocí typu double). Programátor si poté uvědomí, že třída BigDecimal je přesně to, co mu při počítání chybělo a že supertřída Number se dá využít na spoustu zajímavých věcí.

Na této úrovni už stojí za to hledat při řešení problémů různé zkratky a pomůcky.

Vyšší matematika – Apache commons Math

Asi nejznámější implementací dodatečných matematických úloh z různých oblastí je knihovna Math z projektu Apache Commons. Obsahuje funkce pro statistiku, lineární algebru, numerickou analýzu, odhad parametrů a další oblasti matematiky.

Logistická křivka – příklad použití estimatoru

Nedávno jsem řešil výpočet logistické křivky a na tuto úlohu se hodí využít parametrický estimator z knihovny Math. Zdrojový kód vypadá takto:

/**
* Implementace vypoctu logisticke krivky (Wachstumskurve)
* s vyuzitim knihovny Math z Apache commons.
* <p>Logisticka krivka ma rovnici<br/>
* <code>f(x) = s / (1 + e^-z)</code><br/>
* Pro promennou <code>z</code> je pouzit linearni model, tedy<br/>
* <code>f(x) = s / (1 + b * e^(-a * x))</code>
* <code>s, a, b</code> jsou parametry modelu, ktere maji byt spocitany (odhadnuty).</p>
* <p>Trida LogisticProblem pouziva k reseni problemu nejmensich ctvercu
* Levenberg-Marquardt Estimator (pozor na licenci podminky).</p>
* @author Josef Cacek
*/
public class LogisticProblem extends SimpleEstimationProblem {

    //Parametry logisticke funkce
    private EstimatedParameter s;
    private EstimatedParameter a;
    private EstimatedParameter b;

    //pomocne promenne
    private double maxValue = 0d;
    private boolean solved = false;

    /**
     * Pridava jednu namerenou hodnotu.
     * @param aXValue
     * @param aYValue namerena hodnota pro dane X
     * @param aWeight vaha mereni
     */
    public void addValue(double aXValue, double aYValue, double aWeight) {
        double tmpXVal = recomputeXval(aXValue);
        if (Math.abs(aYValue)>Math.abs(maxValue)) {
            maxValue = aYValue;
        }
        addMeasurement(new LocalMeasurement(tmpXVal, aYValue, aWeight));
    }

    /**
     * Vytvari Estimator a vypocita odhad. Vraci RMS.
     * @return RMS (Root Mean Square value)
     * @throws ProblemNotSolvedException kdyz se vyskytne problem pri estimaci
     */
    public double solve() throws ProblemNotSolvedException {
        if (solved) {
            throw new IllegalStateException("Uz je to udelano, uz je to hotovo.");
        }
        solved = true;

        if (getMeasurements().length<3) {
            throw new ProblemNotSolvedException("Zadano prilis malo namerenych hodnot.");
        }

        //pridej parametry modelu a nastav pocatecni hodnoty pred iteraci
        this.s = new EstimatedParameter("s", maxValue*2);
        this.a = new EstimatedParameter("a", 1d);
        this.b = new EstimatedParameter("b", 1d);
        addParameter(this.s);
        addParameter(this.a);
        addParameter(this.b);

        double RMS;
        try {
//          GaussNewtonEstimator estimator = new GaussNewtonEstimator(50, 1.0e-3, 1.0e-3);
            LevenbergMarquardtEstimator estimator = new LevenbergMarquardtEstimator();
            estimator.estimate(this);
            RMS = estimator.getRMS(this);
        } catch (EstimationException ee) {
            ee.printStackTrace();
            throw new ProblemNotSolvedException(ee);
        }

        return RMS;
    }

    /**
     * Tato metoda muze implementovat prepocet hodnot na ose X. Napriklad, kdyz pracujeme
     * s hodnotami, kde x znaci rok. je vhodne "posunout zacatek letopoctu". Napriklad,
     * zacinaji-li namerene hodnoty rokem 2000, odecteme v teto funkci od X hodnotu 2000.
     * @param aXval originalni hodnota na ose X
     * @return prepocitana hodnota X
     */
    private double recomputeXval(double aXval) {
        //v nasem prikladu nemusime nic menit
        return aXval;
    }

    /**
     * Gets computed theoretical value for given timepoint.
     * @param aTimeId
     * @param aMaxPeriods
     * @return
     */
    public double getValue(double aXval) {
        final double tmpXVal = recomputeXval(aXval);
        return theoreticalValue(tmpXVal);
    }


    /**
     * Estimated parameter S of logistic problem
     * @return estimated parameter
     */
    public double getS() {
        return s.getEstimate();
    }

    /**
     * Estimated parameter A of logistic problem
     * @return estimated parameter
     */
    public double getA() {
        return a.getEstimate();
    }

    /**
     * Estimated parameter B of logistic problem
     * @return estimated parameter
     */
    public double getB() {
        return b.getEstimate();
    }


    /**
     * Pocita teoretickou hodnotu pro dane X.
     * @param aXval hodnota X
     * @return vysledná hodnota funkce f(x)
     */
    public double theoreticalValue(double aXval) {
        // f(x, s, a, b)
        return s.getEstimate() / (1d + b.getEstimate()*Math.exp(-a.getEstimate()*aXval));
    }

    /**
     * Pocita hodnotu parcialnich derivaci pro dane x podle daneho parametru.
     * @param aXval hodnota X
     * @param aParam parametr, podle ktereho budeme derivovat
     * @return hodnota parcialni derivace v x podle daneho parametru
     */
    private double partial(double aXval, EstimatedParameter aParam) {
        //tady nezbude programatorovi nic jineho nez oprasit znalosti matematicke
        //analyzy a spocitat si parcialni derivace pozadovane funkce podle vsech
        //pouzitych parametru
        if (aParam == s) {
            // Partial Derivative with respect to s
            return 1d / (1d + b.getEstimate()*Math.exp(-a.getEstimate()*aXval));
        } else if (aParam == a) {
            // Partial Derivative with respect to a
            return s.getEstimate()*b.getEstimate()*aXval*Math.exp(-a.getEstimate()*aXval)/
            Math.pow(1d + b.getEstimate()*Math.exp(-a.getEstimate() * aXval), 2d);
        } else {
            // Partial Derivative with respect to b
            return -s.getEstimate()*Math.exp(-a.getEstimate()*aXval)/
            Math.pow(1d + b.getEstimate()*Math.exp(-a.getEstimate() * aXval), 2d);
        }
    }

    /**
     * Implementace namerenych hodnot - rozsiruje tridu WeightedMeasurement.
     * Mereni je hodnota y pro dane vazane x.
     */
    private class LocalMeasurement extends WeightedMeasurement {

        private static final long serialVersionUID = 0L;
        private final double x;

        /**
         * Constructor
         * @param x measured X
         * @param y measured Y
         * @param w weighth
         */
        public LocalMeasurement(double x, double y, double w) {
            super(w, y);
            this.x = x;
        }

        /**
         * @see org.apache.commons.math.estimation.WeightedMeasurement#getTheoreticalValue()
         */
        public double getTheoreticalValue() {
            // the value is provided by the model for the local x
            return theoreticalValue(x);
        }

        /**
         * @see org.apache.commons.math.estimation.WeightedMeasurement#getPartial(org.apache.commons.math.estimation.EstimatedParameter)
         */
        public double getPartial(EstimatedParameter parameter) {
            // the value is provided by the model for the local x
            return partial(x, parameter);
        }

    }

}

Jak už je poznamenáno ve zdrojovém kódu, je třeba dát si pozor na licenční podmínky u třídy LevenbergMarquardtEstimator a uvést příslušnou informaci v dokumentaci vašeho softwaru.

Použití této třídy může vypadat následovně:

package cz.cacek.logisticproblem;

import org.apache.commons.math.random.RandomData;
import org.apache.commons.math.random.RandomDataImpl;

/**
* Priklad pouziti generatoru nahodnych cisel z commons math
* a vypoctu logisticke krivky pro vygenerovanou nahodnou radu.
*
* @author Josef Cacek
*/
public class TestLogisticProblem {


    /**
     * Vypise na standardni vystup pole cisel
     * @param aDesc popis hodnot
     * @param aValues hodnoty k vypsani
     */
    public static void printArray(String aDesc, double[] aValues) {
        System.out.println("f(x) = " + aDesc + ":");
        for (int i = 0; i < aValues.length; i++) {
            System.out.print("f(" + i + ")=" + aValues[i] + ", ");
        }
    }

    /**
     * Vygeneruje ciselnou radu a resi logisticky problem pro tuto radu.
     * @param args
     */
    public static void main(String args[]) {
        RandomData randomData = new RandomDataImpl();
        double[] values = new double[15];
        LogisticProblem lp = new LogisticProblem();
        for (int i = 0; i < values.length; i++) {
            values[i] = randomData.nextUniform(0d, 10000d);
            lp.addValue(i, values[i], 1d);
        }
        printArray("Random values", values);
        try {
            double rms = lp.solve();
            System.out.println("RMS = " + rms);
            double[] lcValues = new double[15];
            for (int i = 0; i < lcValues.length; i++) {
                lcValues[i] = lp.theoreticalValue(i);
            }
            printArray("Logistic curve values", lcValues);
        } catch (ProblemNotSolvedException e) {
            //tady se nam muze stat napr toto:
            //EstimationException: maximal number of evaluations exceeded (1,000)
            e.printStackTrace();
        }
    }

}

Pro úplnost ještě třída s výjimkou, použitou v progamu:

package cz.cacek.logisticproblem;

/**
* Exception used for reporting problems during solving LogisticProblem
* (Wachstumskurve)
*
* @author Josef Cacek
*/
public class ProblemNotSolvedException extends Exception {

    private static final long serialVersionUID = -81388704034021304L;

    public ProblemNotSolvedException() {
    }

    public ProblemNotSolvedException(String message) {
        super(message);
    }

    public ProblemNotSolvedException(Throwable cause) {
        super(cause);
    }

    public ProblemNotSolvedException(String message, Throwable cause) {
        super(message, cause);
    }

}
Archiv se zdrojovými kódy příkladu si můžete stáhnout zde.

středa 18. června 2008

EBookME 2.5 a JSignPdf 0.7

Trocha reklamy po ránu

Mé open source Java projekty EBookME a JSignPdf se dočkaly nových verzí. Neváhejte je vyzkoušet a okomentovat.

EBookME je aplikace, která slouží k vytváření elektronických knih pro mobilní zařízení (mobily, PDA). Skládá se ze dvou modulů - packager a reader. Packager je Java SE aplikace (Swing/Console/Applet), která generuje ze vstupního textu aplikace pro mobily. Základ pro tyto mobilní aplikace (ebooky) tvoří modul Reader, což je Java ME MIDlet pracující nad MIDP-1.0, CLDC-1.0.

Novinkami ve verzi EBookME 2.5 jsou uživatelem definované bookmarky v Readeru, možnost ukládání projektu v Packageru, lepší integraci do Windows a experimentální podporu Tamilštiny (chuťovka v Javě).

JSignPdf je Swingove GUI pro digitální podepisování PDF dokumentů. Využívá knihovnu iText a zvládá pracovat i s kódovanými dokumenty. Verze 0.7 přidává podporu viditelných podpisů (text nebo ikona na některé stránce PDF). Do další verze je plánováno i ověřování podpisů v PDF.

Obě aplikace jsou lokalizovatelné a samozřejmě nechybí verze v češtině a angličtině.

pátek 28. března 2008

A všichni si rozumíme 2 - klávesové zkratky

Psali jste někdy lokalizované aplikace ve Swingu? A jak jste řešili klávesové zkratky (Mnemonics) pro různé jazyky? Já jsem se při programování JSignPdf rozhodl použít přístup, který znám z Delphi. V resource řetězcích vložím ampersand (&) před písmeno, které má být použito jako klávesová zkratka. Když chci zobrazit znak ampersand, tak napíšu dva za sebe.

A jak to funguje v kódu? Mám wrapper pro ResourceBundle, který nabízí mimo metod pro přístup k lokalizovaným stringům i metodu, která vrací MnemonicIndex (pozici znaku, který je použit jako klávesová zkratka).

/**
* Returns message for given key from active ResourceBundle
* @param aKey name of key in resource bundle
* @return message for given key
*/
public String get(final String aKey) {
  String tmpMessage = bundle.getString(aKey);
  if (tmpMessage == null) {
    tmpMessage = aKey;
  } else {
    tmpMessage = tmpMessage.replaceAll("&([^&])", "$1");
  }
  return tmpMessage;
}

/**
* Returns index of character which should be used as a mnemonic.
* It returns -1 if such an character doesn't exist.
* @param aKey resource key
* @return index (position) of character in translated message
*/
public int getMnemonicIndex(final String aKey) {
  String tmpMessage = bundle.getString(aKey);
  int tmpResult = -1;
  if (tmpMessage != null) {
    int searchFrom = 0;
    int tmpDoubles = 0;
    int tmpPos;
    final int tmpLen = tmpMessage.length();
    do {
      tmpPos = tmpMessage.indexOf('&', searchFrom);
      if (tmpPos == tmpLen-1) tmpPos = -1;
      if (tmpPos>-1) {
        if (tmpMessage.charAt(tmpPos+1) != '&') {
          tmpResult = tmpPos - tmpDoubles;
        } else {
          searchFrom = tmpPos + 2;
          tmpDoubles++;
        }
      }
    } while (tmpPos!=-1 && tmpResult==-1 && searchFrom<tmpLen);
  }
  return tmpResult;
}

/**
* Returns message for given key from active ResourceBundle and replaces
* parameters with values given in array.
* @param aKey key in resource bundle
* @param anArgs array of parameters to replace in message
* @return message for given key with given arguments
*/
public String get(String aKey, String anArgs[]) {
  String tmpMessage = get(aKey);
  if (aKey==tmpMessage || anArgs == null || anArgs.length == 0) {
    return tmpMessage;
  }
  final MessageFormat tmpFormat = new MessageFormat(tmpMessage);
  return tmpFormat.format(anArgs);
}

použití uvedeného wrapperu potom vypadá následovně (res je jméno instance wrapperu):

/**
* Application translations.
*/
private void translateLabels() {
  setTitle(res.get("gui.title", new String[] {Constants.VERSION}));
  setLabelAndMnemonic(lblKeystoreType, "gui.keystoreType.label");
  setLabelAndMnemonic(chkbAdvanced, "gui.advancedView.checkbox");
  setLabelAndMnemonic(btnSignIt,"gui.signIt.button");
}

/**
* Sets translations and mnemonics for labels and different kind of buttons
* @param aComponent component in which should be label set
* @param aKey message key
*/
private void setLabelAndMnemonic(final JComponent aComponent, final String aKey) {
  final String tmpLabelText = res.get(aKey);
  final int tmpMnemIndex = res.getMnemonicIndex(aKey);
  if (aComponent instanceof JLabel) {
    final JLabel tmpLabel = (JLabel) aComponent;
    tmpLabel.setText(tmpLabelText);
    if (tmpMnemIndex>-1) {
      tmpLabel.setDisplayedMnemonic(tmpLabelText.toLowerCase().charAt(tmpMnemIndex));
      tmpLabel.setDisplayedMnemonicIndex(tmpMnemIndex);
    }
  } else if (aComponent instanceof AbstractButton) {
    //handles Buttons, Checkboxes and Radiobuttons
    final AbstractButton tmpBtn = (AbstractButton) aComponent;
    tmpBtn.setText(tmpLabelText);
    if (tmpMnemIndex>-1) {
      tmpBtn.setMnemonic(tmpLabelText.toLowerCase().charAt(tmpMnemIndex));
    }
  } else {
    throw new IllegalArgumentException();
  }
}

Resource fajl potom obsahuje takovéto záznamy:

gui.advancedView.checkbox=A&dvanced view
gui.alias.label=Key &alias

středa 23. ledna 2008

Acegi - logujeme loginy

Používáte-li pro správu přístupu k vaší webové aplikaci framework Acegi, možná se vám bude hodit zaznamenávat uživatelské přístupy (platné loginy) někam do databáze. Zde je jeden ze způsobů jak se s tímto problémem vypořádat. Následující přiklad používá Hibernate a databázi Oracle.

Nejdříve si připravím vlastní metodu pro zápis do databáze v DAO. Umístím ji do třídy cz.mujpackage.dao.UserDao, která rozšiřuje org.springframework.orm.hibernate3.support.HibernateDaoSupport a poskytuje metody pro správu uživatelů, rolí, apod. Pro zvýšení výkonu použiji v Hibernate SQLQuery namísto vytváření instance třídy modelu a jejího ukládání pomocí metody save(...).

/**
* Adds log entry to table AUTH_LOG (Oracle database form - pk_sequence has to be configured)
* @param aName username
* @param aRemoteAddress remote address of request
*/
public void logAuthenticationSuccess(final String aName, final String aRemoteAddress) {
  final HibernateCallback callback = new HibernateCallback() {
    public Object doInHibernate(final Session session) throws HibernateException {
      return session
        .createSQLQuery("insert into AUTH_LOG (id, datum, user, address)" +
            " values (pk_sequence.nextval, sysdate, :user, :address)")
        .setParameter("user", aName)
        .setParameter("address", aRemoteAddress)
        .executeUpdate();
    }
  };
  getHibernateTemplate().execute(callback);
}

Nyní přijde hlavní část a to zachycení události, která je vyvolána při úspěšném přihlášení uživatele do systému. Vytvořím tedy implementaci interfejsu org.springframework.context.ApplicationListener a budu obsluhovat události typu InteractiveAuthenticationSuccessEvent.

package cz.mujpackage.acegi;

import org.acegisecurity.Authentication;
import org.acegisecurity.context.SecurityContextHolder;
import org.acegisecurity.event.authentication.InteractiveAuthenticationSuccessEvent;
import org.acegisecurity.ui.WebAuthenticationDetails;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.util.Assert;

import cz.mujpackage.dao.UserDao;


/**
* Logs the successful authentication to database table
* @author Josef Cacek
*/
public class MyAuthListener implements ApplicationListener, InitializingBean {

  protected final Log log = LogFactory.getLog(getClass());

  private UserDao userDao;

  /**
   * Writes entry to AUTH_LOG table when InteractiveAuthenticationSuccessEvent comes
   * @see org.springframework.context.ApplicationListener#onApplicationEvent(org.springframework.context.ApplicationEvent)
   */
  public void onApplicationEvent(ApplicationEvent event) {
        if (event instanceof InteractiveAuthenticationSuccessEvent) {
             final Authentication tmpAuth = SecurityContextHolder.getContext().getAuthentication();
             log.debug("InteractiveAuthenticationSuccessEvent: " + tmpAuth.getName());
             if (tmpAuth.getDetails() instanceof WebAuthenticationDetails) {
               final WebAuthenticationDetails webDetails =
                 (WebAuthenticationDetails) tmpAuth.getDetails();
               userDao.logAuthenticationSuccess(tmpAuth.getName(),webDetails.getRemoteAddress());
             } else {
               log.warn("Authentication.getDetails() not instance of WebAuthenticationDetails: "
                   + tmpAuth.getDetails());
             }
        }
    }

  /**
   * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
   */
  public void afterPropertiesSet() throws Exception {
    Assert.notNull(userDao);
  }

  /**
   * @return the userDao
   */
  public UserDao getUserDao() {
    return userDao;
  }

  /**
   * @param userDao the userDao to set
   */
  public void setUserDao(UserDao userDao) {
    this.userDao = userDao;
  }

}

Teď už zbývá pouze nakonfigurovat beanu v applicationContext.xml a je hotovo:

<bean id="myAuthListener" class="cz.mujpackage.acegi.MyAuthListener">
    <property name="userDao" ref="userDao"/>
</bean>

A jak by řekl Forrest Gump

....a to je asi tak vše, co vím o krevetách

pátek 18. ledna 2008

Jak na Excel 5 - knihovna JExcelApi

Konečně se v našem seriálu dostáváme k čistě javovým implemtacím Excelového formátu. Začneme knihovnou Java Excel API (JExcelApi), kterou vytvořil Andy Khan a uvolnil pod licencí GNU LGPL.

Co tato knihovna umí zajímavého?

  • vytváření a čtení XLS souborů
  • formátování buněk
  • práci se vzorci
  • vkládání PNG obrázků
  • kopírování grafů (vytváření nových podporováno není)
Knihovna není nijak rozsáhlá, takže se dá v API pohodlně orientovat.

Zajímavější pro vás může být informace, co mi na této knihovně vadí. Tady jsou mé připomínky:

  • odlišný přístup k dokumentům při čtení a při zápisu (třídy Workbook a WritableWorkbook nemají ani společný interface)
  • práce s čisly podporuje pouze typ double, osobně bych uvítal např. i BigDecimal
  • chybí jakákoliv podpora pro makra (ani při kopírování nezůstanou zachována)
  • nepodporuje automatickou šířku sloupce (Auto Fit Selection)
  • nelze nastavit typ buňky (viz příklad - vložím vzorec jehož výsledkem je text, ale JExcelApi si při čtení myslí, že to je číslo)
  • tvar vzorců neodpovídá na 100% tvaru z Excelu (chybí uvozovací rovnítko, jako oddělovač parametrů funkcí je použita čárka namísto středníku)
Další věc, která se mi moc nelíbí (a to ani v originálním Excelu) jsou překlady jmen funkcí používaných ve vzorcích. Sice jde jednoduše nastavit, že se použijí původní "americká" jména (WorkbookSettings.setLocale(...)), ale osobně bych to preferoval úplně bez překladů.

A nakonec praktická ukázka:

/**
* Priklad cteni dokumentu pres JExcel
* @throws BiffException
* @throws IOException
* @throws FormulaException
*/
public static void readDemo() throws BiffException, IOException, FormulaException {
    System.out.println("Ctu soubor: " + FILE_NAME);

    // otevreni souboru
    final Workbook wb = Workbook.getWorkbook(new File(FILE_NAME));

    //vypisem vsechny listy
    for (int i=0,m=wb.getNumberOfSheets();i<m;i++) {
        final Sheet sheet = wb.getSheet(i);
        System.out.println("List: " + sheet.getName());

        //vypisem hodnoty v listu
        for (int j=0,n=sheet.getRows();j<n;j++) {
            final Cell[] cells = sheet.getRow(j);
            final StringBuffer tmpStrRow = new StringBuffer();
            for (int k=0; k<cells.length; k++) {

                //spravne bychom tady meli bunku zpracovavat v zavislosti na jejim typu,
                //neb v JavaDocu je metoda getContents oznacena jako "dirty" :)
                tmpStrRow.append(cells[k].getContents());
                //zracovani podle typu bunky muze vypadat nasledovne:
                if (cells[k].getType()==CellType.BOOLEAN_FORMULA
                        || cells[k].getType()==CellType.DATE_FORMULA
                        || cells[k].getType()==CellType.NUMBER_FORMULA
                        || cells[k].getType()==CellType.STRING_FORMULA) {
                    final FormulaCell tmpFormula = (FormulaCell) cells[k];
                    //do zavorky vypiseme vzorech pouzity v teto bunce
                    tmpStrRow.append(" (").append(tmpFormula.getFormula()).append(")");
                }
                tmpStrRow.append("\t");
            }
            System.out.println(tmpStrRow.toString());
        }
    }
    //nakonec zavrem workbook  a uvolnime pamet
    wb.close();
}

/**
* Priklad vytvoreni XLS dokumentu pres JExcel
* @throws IOException see {@link Workbook}
* @throws RowsExceededException see {@link WritableSheet#addCell(jxl.write.WritableCell)}
* @throws WriteException see {@link WritableSheet#addCell(jxl.write.WritableCell)}
*/
public static void writeDemo() throws IOException, RowsExceededException, WriteException {
    System.out.println("Vytvarim soubor: " + FILE_NAME);
    // konfigurace - nechci pouzivat jmena funkci v nemcine :)
    final WorkbookSettings wbs = new WorkbookSettings();
    wbs.setLocale(Locale.US);
    //vytvoreni noveho Workbooku (souboru)
    final WritableWorkbook wb = Workbook.createWorkbook(new File(FILE_NAME),wbs);
    //pridame list
    final WritableSheet sheet = wb.createSheet("Muj super list", 0);
    //nastaveni fontu
    final WritableFont font = new WritableFont(WritableFont.TIMES, 16, WritableFont.BOLD, true);
    final WritableCellFormat cellFormat = new WritableCellFormat(font);

    //pridani stringu
    sheet.addCell(new Label(0,0,"Popisek hodnoty", cellFormat));
    //pridani cisla (bez specialniho formatovani)
    sheet.addCell(new Number(1,0, 1.23456d));
    //pridani vzorce
    sheet.addCell(new Formula(1,1, "CONCATENATE(\"Vysledek vzorce: \",B1+1.90703)"));

    //zapsani do souboru
    wb.write();
    //uvolneni z pameti
    wb.close();
    System.out.println("Soubor ulozen.");
}
výstupem programu bylo na mém německém počítači toto:
Vytvarim soubor: jne5.xls
Soubor ulozen.
Ctu soubor: jne5.xls
List: Muj super list
Popisek hodnoty 1,235 
 4 (VERKETTEN("Vysledek vzorce: ",B1+1.90703))
Celý příklad si můžete stáhnout na adrese http://www.cacek.cz/javlog-attachments/JakNaExcel5.zip

čtvrtek 3. ledna 2008

OpenOffice.org - spojení obsahu buněk

Tak se po prosincové dovolené vracím k počítači a první záznam v novém roce se nebude zabývat Javou, ale OpenOffice.org. V konferenci users@cs.openoffice.org se řešilo, jak poskládat dohromady hodnoty buněk ze sloupce Calcu do jednoho stringu s oddělovačem mezi hodnotami.

První nápad, který se objevil bylo využití klasické funkce CONCATENATE:

=CONCATENATE(A1;"oddelovac";A2;"oddelovac";A3)

Už toto může být dost náročné při více hodnotách ve sloupci. Ale v konferenci přibyl požadavek na ošetření prázdných buněk, které nemají být ve výsledném seznamu. Nový kód byl:

=CONCATENATE(IF(NOT(A1="");CONCATENATE(A1;",");"");IF(NOT(A2="");CONCATENATE(A2;",");"");IF(NOT(A3="");CONCATENATE(A3;",");""))

Tady už se v tom začíná člověk ztrácet navíc nám za poslední hodnotou zůstává oddělovač. Tak vylepšíme:

=LEFT(CONCATENATE(IF(NOT(A1="");CONCATENATE(A1;",");"");IF(NOT(A2="");CONCATENATE(A2;",");"");IF(NOT(A3="");CONCATENATE(A3;",");"")); LEN(CONCATENATE(IF(NOT(A1="");CONCATENATE(A1;",");"");IF(NOT(A2="");CONCATENATE(A2;",");"");IF(NOT(A3="");CONCATENATE(A3;",");""))) - 1 )

Tohle už je nepoužitelné. Takže začneme úplně znovu. Do dokumentu si vložíme makro (Nástroje > Makra > Správce maker > OpenOffice.org Basic...)

Public Function MyConcat(aRange, aSplitter as String)
 Dim tmpResult As String
 Dim i, j as Integer

 tmpResult = ""

 For i = LBound(aRange(),2) To UBound(aRange(),2)
   For j = LBound(aRange()) To UBound(aRange())
     if aRange(j,i) <> "" And aRange(j,i) <> 0 then
       if tmpResult <> "" then
         tmpResult = tmpResult + aSplitter
       end if
       tmpResult = tmpResult + aRange(j, i)
     end if
   Next
 Next

 MyConcat = tmpResult
End Function

A nyní už můžeme vesele použít konstrukci:

=MyConcat(A1:A3;",")

Více informací o práci s makry můžete najít v OpenOffice.org How-To dokumentech. Doporučuji podívat se hlavně na: